Your Turn

One-Way Calls

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.

otp-server/1/sequence/lib/sequence/server.ex
 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

Tracing a Server’s Execution

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.)

otp-server/1/sequence/lib/sequence/server.ex
 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"}]
  ]
  ]}