Elixir 1.6

Chapter 18 

OTP: Servers

OTP

OTP stands for the Open Telecom Platform
It was initially used to build telephone exchanges and switches. OTP is now a general-purpose tool for developing and managing large systems.

 

OTP is actually a bundle that includes Erlang, a database (wonderfully called Mnesia), and an innumerable number of libraries.

It also defines a structure for your application.

OTP

OTP defines systems in terms of hierarchies of applications.
An application consists of one or more processes.
These processes follow one of a small number of OTP conventions, called behaviors.

There is a behavior used for general-purpose servers,
one for implementing event handlers,
and one for finite-state machines.
Each implementation of one of these behaviors
will run in its own process(es).

Another special behavior, called supervisor,
monitors the health of these processes and implements strategies for restarting them if needed.

We’ll be implementing the server behavior called GenServer.

An OTP Server

When we write an OTP server, we write a module containing one or more callback functions with
standard names. OTP will invoke the appropriate callback to handle a particular situation.

When someone sends a request to our server,
OTP will call our handle_call function, passing in the request, the caller, and the current server state.

Our function responds by returning a tuple
containing an action to take,
the return value for the request,
and an updated state.

Our First OTP Server

$ ​​mix​​ ​​new​​ ​​sequence​
​* creating README.md
​* creating .gitignore
​* creating mix.exs
​* creating lib
​* creating lib/sequence.ex
​* creating lib/sequence
​* creating test
​* creating test/test_helper.exs
​* creating test/sequence_test.exs

Create the Basic Sequence Server

When a client calls our server,
GenServer invokes the handle_call function.
It receives the information as its first parameter,
the PID of the client as the second parameter,
and the server state as the third parameter.

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  
end

Create the Basic Sequence Server

We return a tuple to OTP:

​{ ​:reply​, current_number, current_number+1 }

The :reply tells OTP to reply to the client, passing back the value that is the second element. The tuple’s third element defines the new state. This will be passed as the last parameter to handle_call the next time it is invoked.

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  
end

Fire Up Our Server Manually

The start_link function behaves like the spawn_link.
It asks GenServer to start a new process and link to us.
We pass in the module and the initial state.
We get back a status (:ok) and the server’s PID. The call function takes this PID and calls the handle_call function in the server. The call’s second parameter is passed as the first argument to handle_call.

$ iex -S mix
​​iex>​ { ​:ok​, pid } = GenServer.start_link(Sequence.Server, 100)
​{:ok,#PID<0.71.0>}
​​iex>​ GenServer.call(pid, ​:next_number​)
​100
​​iex>​ GenServer.call(pid, ​:next_number​)
​101
​​iex>​ GenServer.call(pid, ​:next_number​)

Fire Up Our Server Manually

The only value we need to pass is the identity of the action we want to perform, :next_number.
If you look at the definition of handle_call in the server, you’ll see that its first parameter is :next_number

 

$ iex -S mix
​​iex>​ { ​:ok​, pid } = GenServer.start_link(Sequence.Server, 100)
​{:ok,#PID<0.71.0>}
​​iex>​ GenServer.call(pid, ​:next_number​)
​100
​​iex>​ GenServer.call(pid, ​:next_number​)
​101
​​iex>​ GenServer.call(pid, ​:next_number​)

Fire Up Our Server Manually

When Elixir invokes the function, it pattern-matches the argument in the call with this first parameter in the function.  A server can support multiple actions by implementing multiple handle_call functions with different first parameters.

 

defmodule Sequence.Server do
  use GenServer                      

  def handle_call(:next_number, _from, current_number) do        
    { :reply, current_number, current_number+1 }
  end

  def handle_call(:next_next_number, _from, current_number) do        
    { :reply, current_number, current_number+2 }
  end
end

Fire Up Our Server Manually

If you want to pass more than one thing in the call to a server, pass a tuple. For example, our server might need a function to reset the count to a given value.

A handler can return multiple values
by packaging them into a tuple or list.

# handle multiple values in a tuple
def​ handle_call({​:set_number​, new_number}, _from, _current_number) ​do​
​  { ​:reply​, new_number, new_number }
​​end​

iex>​ GenServer.call(pid, {​:set_number​, 999})
999

# return multiple values in a tuple
​def​ handle_call({​:factors​, number}, _, _) ​do​
​  { ​:reply​, { ​:factors_of​, number, factors(number)}, [] }
​​end​


One-Way Calls

The call function calls a server and waits for a reply. What if you don’t want to wait because there is no reply coming back? In those circumstances,
use the GenServer cast function. 

Just like call is passed to handle_call in the server, cast is sent to handle_cast. handle_cast only takes two parameters: the call argument and the current state.
It will return the tuple
{:noreply, new_state}.

One-Way Calls

Notice that handle_cast takes in a tuple as it's first param. The first element is :increment_number
which is used to pattern match.
The function simply returns a tuple.

defmodule Sequence.Server do
  use GenServer                      

  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

One-Way Calls

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

​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

Tracing a Server’s Execution

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: {{2013,4,26},{18,17,16}}, current_time: {{2013,4,26},{18,17,28}},
​      reductions: 50, messages_in: 2, messages_out: 0]}

Timestamps are given as {{y,m,d},{h,m,s}} tuples.
And the reductions value is a measure of the amount of work the server does.

 

Tracing a Server’s Execution

The Erlang sys module is your interface to the world of system messages.
The debug parameter you give to GenServer is simply the names of functions to call in the sys module.
You can turn things on and off
after you have started a server.

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

Tracing a Server’s Execution

get_status is another useful sys function.

 

​iex>​ ​:sys​.get_status pid
​{:status,#PID<0.57.0>,{:module,:gen_server},[["$ancestors": [#PID<0.25.0>],
​"$initial_call":
​{Sequence.Server,:init,1}],:running,#PID<0.25.0>,[],
​[header: 'Status for generic server <0.57.0>',
​data: [{'Status',:running},{'Parent',#PID<0.25.0>},{'Logged events',[]}],
data: [{'State',102}]]]}

Tracing a Server’s Execution

You have the option to change the ’State’ part to return a more application-specific message by defining format_status.

def​ format_status(_reason, [ _pdict, state ]) ​do​
​  [​data:​ [{​'State'​, ​"​​My current state is '​​#{​inspect state​}​​', and I'm happy"​}]]
​​end​

​​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"}]
   ]
 ]
}

GenServer Callbacks

GenServer is an OTP protocol. OTP works by assuming that your module defines a number of callback functions (six, in the case of a GenServer).

When you add the line use GenServer to a module, Elixir creates default implementations of these six callback functions

init(start_arguments)
handle_call(request, from, state)
handle_cast(request, state)
handle_info(info, state)
terminate(reason, state)
code_change(from_version, state, extra)
format_status(reason, [pdict, state]) 

GenServer Callbacks

init(start_arguments)

  • Called when starting a new server.
  • The parameter is the second argument passed to start_link. Should return {:ok, state} on success, or {:stop, reason} if the server could not be started.
  • You can specify an optional timeout using
    {:ok, state, timeout}, then GenServer sends the process a :timeout message whenever no message is received in a span of timeout ms. (The message is passed to the handle_info function.)
  • The default GenServer implementation sets the server state to the argument you pass.

GenServer Callbacks

handle_call(request, from, state)

  • Invoked when a client uses GenServer.call(pid, request). The from parameter is a tuple containing the PID of the client and a unique tag. The state parameter is the server state.
  • On success returns {:reply, result, new_state}
  • The default implementation stops the server with a :bad_call error, so you’ll need to implement handle_call for every call request type your server implements.

GenServer Callbacks

handle_cast(request, state)

  • Called in response to GenServer.cast(pid, request).
  • A successful response is {:noreply, new_state}. Can also return {:stop, reason, new_state}.
  • The default implementation stops the server with a :bad_cast error.

GenServer Callbacks

handle_info(info, state)

  • Called to handle incoming messages that are not call or cast requests.
  • For example, timeout messages are handled here.
  • So are termination messages from any linked processes.
  • Messages sent to the PID using send (so they bypass GenServer) will be routed to this function.

GenServer Callbacks

terminate(reason, state)

  • Called when the server is about to be terminated.
  • Once we add supervision to our servers, we don’t have to worry about this.

 

GenServer Callbacks

code_change(from_version, state, extra)

  • OTP lets us replace a running server without stopping the system.
  • The new version of the server may represent its state differently from the old version.
  • The code_change callback is invoked to change from the old state format to the new.

 

GenServer Callbacks

format_status(reason, [pdict, state])

  • Used to customize the state display of the server.
  • The conventional response is
    [data: [{’State’, state_info}]].

 

Naming a Process

The idea of referencing processes by their PID gets old quickly. Fortunately, there are a number of alternatives.

The simplest is local naming. We use the name: option.

iex>​ { ​:ok​, pid } = GenServer.start_link(Sequence.Server, 100, ​name:​ ​:seq​)
​{:ok,#PID<0.58.0>}
​​iex>​ GenServer.call(​:seq​, ​:next_number​)
​100
​​iex>​ GenServer.call(​:seq​, ​:next_number​)
​101
​​iex>​ ​:sys​.get_status ​:seq​
​{:status, #PID<0.69.0>, {:module, :gen_server},
​ [["$ancestors": [#PID<0.58.0>],
​   "$initial_call": {Sequence.Server, :init, 1}],
​  :running, #PID<0.58.0>, [],
​  [header: 'Status for generic server seq',
​   data: [{'Status', :running},
​          {'Parent', #PID<0.58.0>},
​          {'Logged events', []}],
​   data: [{'State', "My current state is '102', and I'm happy"}]]]}

Tidying Up the Interface

defmodule Sequence.Server do
  use GenServer

  #####
  # External API  
  def start_link(current_number) do
    GenServer.start_link(__MODULE__, current_number, name: __MODULE__)
  end

  def next_number do
    GenServer.call __MODULE__, :next_number
  end

  def increment_number(delta) do
    GenServer.cast __MODULE__, {:increment_number, delta}
  end

  #####
  # GenServer implementation
  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

  def format_status(_reason, [ _pdict, state ]) do
    [data: [{'State', "My current state is '#{inspect state}', and I'm happy"}]] 
  end
end

Tidying Up the Interface

$ iex -S mix
​​iex>​ Sequence.Server.start_link 123
​{:ok,#PID<0.57.0>}
​​iex>​ Sequence.Server.next_number
​123
​​iex>​ Sequence.Server.next_number
​124
​​iex>​ Sequence.Server.increment_number 100
​:ok
​​iex>​ Sequence.Server.next_number
​225

OTP GenServer

An OTP GenServer is just a regular Elixir process in which the message handling has been abstracted out.

The GenServer behavior defines a message loop internally and maintains a state variable. That message loop then calls out to various functions that we define in our server module: handle_call, handle_cast, and so on.

We also saw that GenServer provides fairly detailed tracing of the messages received and responses.

Finally, we wrapped our message-based API in module functions, which gives our users a cleaner interface and decouples them from our implementation.

Making Our Server Into a Component

It puts three things into a single source file:

  1. The API
  2. The logic of our service (adding one)
  3. The implementation of that logic in a server

 

So this is an experiment to move each to it's own file

defmodule Sequence do
  @server Sequence.Server
  
  def start_link(current_number) do
    GenServer.start_link(@server, current_number, name: @server)
  end

  def next_number do
    GenServer.call(@server, :next_number)
  end

  def increment_number(delta) do
    GenServer.cast(@server, {:increment_number, delta})
  end
end

Public Interface

defmodule Sequence.Server do
  use GenServer
  alias Sequence.Impl
  
  def init(initial_number) do
    { :ok, initial_number }
  end
  
  def handle_call(:next_number, _from, current_number) do
    { :reply, current_number, Impl.next(current_number) }
  end

  def handle_cast({:increment_number, delta}, current_number) do
    { :noreply, Impl.increment(current_number, delta) }
  end

  def format_status(_reason, [ _pdict, state ]) do
    [data: [{'State', "My current state is '#{inspect state}', and I'm happy"}]] 
  end
end

Private Interface

defmodule Sequence.Impl do
  def next(number),             do: number + 1
  def increment(number, delta), do: number + delta
end

Implementation

defmodule Sequence do
  @server Sequence.Server
  
  def start_link(current_number) do
    GenServer.start_link(@server, current_number, name: @server)
  end

  def next_number do
    GenServer.call(@server, :next_number)
  end

  def increment_number(delta) do
    GenServer.cast(@server, {:increment_number, delta})
  end
end

defmodule Sequence.Server do
  use GenServer
  alias Sequence.Impl
  
  def init(initial_number) do
    { :ok, initial_number }
  end
  
  def handle_call(:next_number, _from, current_number) do
    { :reply, current_number, Impl.next(current_number) }
  end

  def handle_cast({:increment_number, delta}, current_number) do
    { :noreply, Impl.increment(current_number, delta) }
  end

  def format_status(_reason, [ _pdict, state ]) do
    [data: [{'State', "My current state is '#{inspect state}', and I'm happy"}]] 
  end
end

defmodule Sequence.Impl do
  def next(number),             do: number + 1
  def increment(number, delta), do: number + delta
end

Thank you!

Programming Elixir 1.6 Chapter 18

By Dustin McCraw

Programming Elixir 1.6 Chapter 18

  • 1,013