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:
- The API
- The logic of our service (adding one)
- 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