Mix.install([
{:youtube, github: "brooklinjazz/youtube"},
{:hidden_cell, github: "brooklinjazz/hidden_cell"},
{:tested_cell, github: "brooklinjazz/tested_cell"},
{:utils, path: "#{__DIR__}/../utils"}
])
Ensure you type the ea
keyboard shortcut to evaluate all Elixir cells before starting. Alternatively, you can evaluate the Elixir cells as you read.
This lesson is highly inspired by Saša Jurić. Saša has graciously allowed us to use code examples directly from his book Elixir in Action.
It's a common pattern to implement a GenericServer
process which can
receive messages, delegate to some module, then send a message back to the caller.
A generic server stores a short-term in-memory state. The state can only be updated by other processes sending messages to the generic server.
flowchart
subgraph GenericServer
State --> h
h[handle message]
h --new state--> State
end
Process --message--> GenericServer
Typically the callback module defines an init
function to define the initial state of
the GenericServer
process.
flowchart LR
CallbackModule --> init --> state --> GenericServer
defmodule GenericServer do
def start(callback_module) do
spawn(fn ->
initial_state = callback_module.init()
loop(callback_module, initial_state)
end)
end
end
defmodule CallbackModule do
def init() do
"example state"
end
end
GenericServer.start(CallbackModule)
The GenericServer
should be able to send a message and receive a response.
flowchart
GS2[Genserver 1]
GS1[Genserver 2]
c[call]
GS1 --> c --request--> GS2
GS2 --response--> c
defmodule GenericServer
def call(server_pid, request) do
send(server_pid, {request, self()})
receive do
{:response, response} ->
response
end
end
...
end
The callback module will also define event handlers for when the GenericServer
receives
a message.
flowchart LR
GenericServer --> receive
receive --request, state--> CallbackModule --> h["handle_call(request, state)"]
--request, new_state --> receive
defmodule CallbackModule do
...
def handle_call(:increment, state) do
new_state = state + 1
response = {:ok, new_state}
{response, new_state}
end
end
The GenericServer
then loops and receives messages while delegating to the CallbackModule
flowchart LR
Caller --> GenericServer --> loop
--> receive --> c[CallbackModule.handle_call] --> response --> send --> Caller
defmodule GenericServer do
...
defp loop(callback_module, current_state) do
receive do
{request, caller} ->
{response, new_state} =
callback_module.handle_call(
request,
current_state
)
send(caller, {:response, response})
loop(callback_module, new_state)
end
end
end
Putting all of that together, we get the full flow of our GenericServer
and CallbackModule
.
flowchart
GenericServer --> loop --state--> receive --new state--> loop
Caller --> c[call] --request--> receive --response--> c
receive --request, state--> CallbackModule --> h["handle_call(request, state)"]
--request, new_state --> receive
style GenericServer fill:orange,stroke:#333,stroke-width:2px
style Caller fill:yellow,stroke:#333,stroke-width:2px
style CallbackModule fill:#f9f,stroke:#333,stroke-width:2px
style receive fill:orange
style loop fill:orange
style c fill:yellow
style h fill:#f9f
We have a full GenericServer
module.
defmodule GenericServer do
def call(server_pid, request) do
send(server_pid, {request, self()})
receive do
{:response, response} ->
response
end
end
def start(callback_module) do
spawn(fn ->
initial_state = callback_module.init()
loop(callback_module, initial_state)
end)
end
defp loop(callback_module, current_state) do
receive do
{request, caller} ->
{response, new_state} =
callback_module.handle_call(
request,
current_state
)
send(caller, {:response, response})
loop(callback_module, new_state)
end
end
end
GenericServer
is incredibly powerful because it lets us reuse generic state and message
passing functionality with more domain-specific callback modules.
Let's re-implement the Counter
with some extra
functionality to use with the GenericServer
.
The Counter
module is now a domain-specific CallbackModule
.
defmodule Counter do
def init() do
0
end
def handle_call(:increment, state) do
new_state = state + 1
response = {:ok, new_state}
{response, new_state}
end
end
counter_process = GenericServer.start(Counter)
Now we can call the counter process to update its internal state.
GenericServer.call(counter_process, :increment)
handle_call/2
lets us define generic messages we can send to the Counter process.
For example, in the message, we could :add
an integer as a payload.
defmodule AddableCounter do
def init() do
0
end
def handle_call({:add, payload}, state) do
new_state = state + payload
response = {:ok, new_state}
{response, new_state}
end
end
add_counter_process = GenericServer.start(AddableCounter)
Here's how we would send the :add
message with a payload.
GenericServer.call(add_counter_process, {:add, 10})
In the Elixir cell below, create a NoteBook
callback module.
A NoteBook
's initial state should be an empty list. It should implement a handle_call/2
function
adds a new note like so.
note_book = GenericServer.start(NoteBook)
{:ok, ["new note 1"]} = GenericServer.call(note_book, {:add_note, "new note 1"})
{:ok, ["new note 1", "new note 2"]} = GenericServer.call(note_book, {:add_note, "new note 2"})
defmodule Notebook do
end
It's useful to build your own generic server for the purpose of understanding how they work. However, we can and should rely on the built-in GenServer provided by Elixir.
We use
the GenServer provided by Elixir. Under the hood, this defines the generic server functionality
for the module.
Then much like our GenericServer
we can define an init
function. The init function
now accepts options, which we could use to set the initial state. We'll ignore _opts
for now.
We use @impl
to specify that init
is a callback function for the GenServer. You can
see the @impl documentation for
more on why.
defmodule Counter do
use GenServer
@impl true
def init(_opts) do
{:ok, 0}
end
end
We use GenServer.start_link/2 to start the new Counter
process with the new counter process.
There are no options, so the second parameter is an empty list.
{:ok, pid} = GenServer.start_link(Counter, [])
We can define handle_call/3
functions. These look mostly the same as before, except
the second parameter will be the caller pid, and the third will be the state.
We also always return {:reply, response, new_state}
in a handle_call/3
function.
defmodule Counter do
use GenServer
@impl true
def init(_opts) do
{:ok, 0}
end
@impl true
def handle_call(:increment, _from, state) do
new_state = state + 1
response = {:ok, new_state}
{:reply, response, new_state}
end
end
{:ok, pid} = GenServer.start_link(Counter, 0)
Now let's test our new Counter
! You can execute this Elixir cell below a few times and notice
that the count increments.
GenServer.call(pid, :increment)
call
executes the handle_call/3
function in the internal code of GenServer, just like our
GenericServer
. The parent process then receives a response.
flowchart
P[Parent Process] --> GenServer --> c[call] --> h[handle_call/3] --response--> P
h --new_state--> GenServer
This is the heart of generic servers. We can create an in-memory process have it store some state. Then, we send the process messages to perform some work and return a response. Generic servers are often the go-to tool for short-term in-memory persistence.
The built-in GenServer also has some additional functionality, which will become more important as you work with concurrency. But, for now, we're going to focus on using it as a tool for persistence, so you have everything you currently need.
In the Elixir cell below, use the built-in GenServer module to create a Journal
module where you can :add_entry
.
All journal entries should be stored in state and returned as a list
for the response.
{:ok, journal_pid} = GenServer.start_link(Journal, [])
GenServer.call(:add_entry, "first entry")
["first entry"]
GenServer.call(:add_entry, "second entry")
["first entry", "second entry"]
defmodule Journal do
end
Run the following in your command line from the beta_curriculum folder to track and save your progress in a Git commit.
$ git add .
$ git commit -m "finish generic server section"
Previous | Next |
---|---|
Processes | Traffic Light Server |