You’re going to start creating a server that implements a stack. The call that initializes your stack will pass in a list of the initial stack contents.
For now, implement only the pop interface. It’s acceptable for your server to crash if someone tries to pop from an empty stack.
For example, if initialized with [5,"cat",9], successive calls to pop will return 5, "cat", and 9.
The call function calls a server and waits for a reply. But sometimes you won’t want to wait because there is no reply coming back. In those circumstances, use the GenServer cast function. (Think of it as casting your request into the sea of servers.)
Just like call is passed to handle_call in the server, cast is sent to handle_cast. Because there’s no response possible, the handle_cast function takes only two parameters: the call argument and the current state. And because it doesn’t want to send a reply, it will return the tuple {:noreply, new_state}.
Let’s modify our sequence server to support an :increment_number function. We’ll treat this as a cast, so it simply sets the new state and returns.
| defmodule Sequence.Server do |
| use GenServer |
| |
| def init(initial_number) do |
| { :ok, initial_number } |
| end |
| |
| def handle_call(:next_number, _from, current_number) do |
| {:reply, current_number, current_number + 1} |
| end |
| |
» | def handle_cast({:increment_number, delta}, current_number) do |
» | { :noreply, current_number + delta} |
» | end |
| end |
Notice that the cast handler takes a tuple as its first parameter. The first element is :increment_number, and is used by pattern matching to select the handlers to run. The second element of the tuple is the delta to add to our state. The function simply returns a tuple, where the state is the previous state plus this number.
To call this from our IEx session, we first have to recompile our source. The r command takes a module name and recompiles the file containing that module.
| iex> r Sequence.Server |
| .../sequence/lib/sequence/server.ex:2: redefining module Sequence.Server |
| {Sequence.Server,[Sequence.Server]] |
Even though we’ve recompiled the code, the old version is still running. The VM doesn’t hot-swap code until you explicitly access it by module name. So, to try our new functionality we’ll create a new server. When it starts, it will pick up the latest version of the code.
| iex> { :ok, pid } = GenServer.start_link(Sequence.Server, 100) |
| {:ok,#PID<0.60.0>} |
| iex> GenServer.call(pid, :next_number) |
| 100 |
| iex> GenServer.call(pid, :next_number) |
| 101 |
| iex> GenServer.cast(pid, {:increment_number, 200}) |
| :ok |
| iex> GenServer.call(pid, :next_number) |
| 302 |
The third parameter to start_link is a set of options. A useful one during development is the debug trace, which logs message activity to the console.
We enable tracing using the debug option:
» | iex> {:ok,pid} = GenServer.start_link(Sequence.Server, 100, [debug: [:trace]]) |
| {:ok,#PID<0.68.0>} |
| iex> GenServer.call(pid, :next_number) |
| *DBG* <0.68.0> got call next_number from <0.25.0> |
| *DBG* <0.68.0> sent 100 to <0.25.0>, new state 101 |
| 100 |
| iex> GenServer.call(pid, :next_number) |
| *DBG* <0.68.0> got call next_number from <0.25.0> |
| *DBG* <0.68.0> sent 101 to <0.25.0>, new state 102 |
| 101 |
See how it traces the incoming call and the response we send back. A nice touch is that it also shows the next state.
We can also include :statistics in the debug list to ask a server to keep some basic statistics:
» | iex> {:ok,pid} = GenServer.start_link(Sequence.Server, 100, [debug: [:statistics]]) |
| {:ok,#PID<0.69.0>} |
| iex> GenServer.call(pid, :next_number) |
| 100 |
| iex> GenServer.call(pid, :next_number) |
| 101 |
| iex> :sys.statistics pid, :get |
| {:ok, |
| [ |
| start_time: {{2017, 12, 23}, {14, 6, 7}}, |
| current_time: {{2017, 12, 23}, {14, 6, 24}}, |
| reductions: 36, |
| messages_in: 2, |
| messages_out: 0 |
| ]} |
Most of the fields should be fairly obvious. Timestamps are given as {{y,m,d},{h,m,s}} tuples. The reductions value is a measure of the amount of work the server does. It is used in process scheduling as a way of making sure all processes get a fair share of the available CPU.
The Erlang sys module is your interface to the world of system messages. These are sent in the background between processes—they’re a bit like the backchatter in a multiplayer video game. While two players are engaged in an attack (their real work), they can also be sending each other background messages: “Where are you?,” “Stop moving,” and so on.
The list associated with the debug parameter you give to GenServer is simply the names of functions to call in the sys module. If you say [debug: [:trace, :statistics]], then those functions will be called in sys, passing in the server’s PID. Look at the documentation for sys to see what’s available.[34]
This also means you can turn things on and off after you have started a server. For example, you can enable tracing on an existing server using the following:
| iex> :sys.trace pid, true |
| :ok |
| iex> GenServer.call(pid, :next_number) |
| *DBG* <0.69.0> got call next_number from <0.25.0> |
| *DBG* <0.69.0> sent 105 to <0.25.0>, new state 106 |
| 105 |
| iex> :sys.trace pid, false |
| :ok |
| iex> GenServer.call(pid, :next_number) |
| 106 |
get_status is another useful sys function:
| iex> :sys.get_status pid |
| {:status, #PID<0.134.0>, {:module, :gen_server}, |
| [ |
| [ |
| "$initial_call": {Sequence.Server, :init, 1}, |
| "$ancestors": [#PID<0.118.0>, #PID<0.57.0>] |
| ], |
| :running, |
| #PID<0.118.0>, |
| [statistics: {{{2017, 12, 23}, {14, 11, 13}}, {:reductions, 14}, 3, 0}, |
| [ |
| header: 'Status for generic server <0.134.0>', |
| data: [ |
| {'Status', :running}, |
| {'Parent', #PID<0.118.0>}, |
| {'Logged events', []} |
| ], |
| data: [{'State', 103}] |
| ] |
This is the default formatting of the status message GenServer provides. You have the option to change the data: part to a more application-specific message by defining a format_status function. This receives an option describing why the function was called, as well as a list containing the server’s process dictionary and the current state. (Note that in the code that follows, the string State in the response is in single quotes.)
| def format_status(_reason, [ _pdict, state ]) do |
| [data: [{'State', "My current state is '#{inspect state}', and I'm happy"}]] |
| end |
If we ask for the status in IEx, we get the new message (after restarting the server):
| iex> :sys.get_status pid |
| {:status, #PID<0.124.0>, {:module, :gen_server}, |
| [ |
| [ |
| "$initial_call": {Sequence.Server, :init, 1}, |
| "$ancestors": [#PID<0.118.0>, #PID<0.57.0>] |
| ], |
| :running, |
| #PID<0.118.0>, |
| [statistics: {{{2017, 12, 23}, {14, 6, 7}}, {:reductions, 14}, 2, 0}], |
| [ |
| header: 'Status for generic server <0.124.0>', |
| data: [ |
| {'Status', :running}, |
| {'Parent', #PID<0.118.0>}, |
| {'Logged events', []} |
| ], |
| data: [{'State', "My current state is '102', and I'm happy"}] |
| ] |
| ]} |