In chapter 5, you became familiar with basic concurrency techniques: you learned how to create processes and communicate with them. I also explained the idea behind stateful server processes—long-running processes that react to messages and maintain state.
Server processes play an important role and are used frequently when building highly concurrent systems in Elixir and Erlang, so we’ll spend some time exploring them in detail. In this chapter, you’ll learn how to reduce some of the boilerplate associated with server processes, such as infinite recursion, state management, and message passing.
Erlang provides a helper module for implementing server processes—it’s part of the Open Telecom Platform (OTP) framework. Despite its misleading name, the framework has nothing to do with telecoms; rather, it provides patterns and abstractions for tasks such as creating components, building releases, developing server processes, handling and recovering from runtime errors, logging, event handling, and upgrading code.
You’ll learn about various parts of OTP throughout this book, but in this chapter, we’ll focus on one of its most important parts: GenServer
, the module that simplifies the implementation of server processes. Before we look at GenServer
, though, you’ll implement a simplified version of it, based on the message-passing primitives you saw in chapter 5.
You saw a few examples of server processes in chapter 5. Although those processes serve different purposes, there are some commonalities in their implementations. In particular, all code that implements a server process needs to do the following:
No matter what kind of server process you run, you’ll always need to do these tasks, so it’s worth moving this code to a single place. Concrete implementations can then reuse this code and focus on their specific needs. Let’s look at how you can implement such generic code.
The generic code will perform various tasks common to server processes, leaving the specific decisions to concrete implementations. For example, the generic code will spawn a process, but the concrete implementation must determine the initial state. Similarly, the generic code will run the loop; receive messages; and, optionally, send the responses, but the concrete implementation must decide how each message is handled and what the response is.
In other words, the generic code drives the entire process, and the specific implementation must fill in the missing pieces. Therefore, you need a plug-in mechanism that lets the generic code call into the concrete implementation when a specific decision needs to be made.
The simplest way to do this is to use modules. Remember that a module name is an atom. You can store that atom in a variable and use the variable later to invoke functions on the module:
iex(1)> some_module = IO ❶ iex(2)> some_module.puts("Hello") ❷ Hello
❶ Stores a module atom in a variable
You can use this feature to provide callback hooks from the generic code. In particular, you can take the following approach:
Make the generic code accept a plug-in module as the argument. That module is called a callback module.
Obviously, for this to work, a callback module must implement and export a well-defined set of functions, which I’ll gradually introduce as we implement the generic code.
Let’s start building a generic server process. First, you need to start the process and initialize its state.
Listing 6.1 Starting the server process (server_process.ex)
defmodule ServerProcess do
def start(callback_module) do
spawn(fn ->
initial_state = callback_module.init() ❶
loop(callback_module, initial_state)
end)
end
...
end
❶ Invokes the callback to initialize the state
ServerProcess.start/1
takes a module atom as the argument and then spawns the process. In the spawned process, the callback function init/0
is invoked to create the initial state. Obviously, for this to work, the callback module must export the init/0
function.
Finally, you enter the loop that will power the server process and maintain this state. The return value of ServerProcess.start/1
is a pid
, which can be used to send messages to the request process.
Next, you need to implement the loop code that powers the process, waits for messages, and handles them. In this example, you’ll implement a synchronous send-and-respond communication pattern. The server process must receive a message, handle it, send the response message back to the caller, and change the process state.
The generic code is responsible for receiving and sending messages, whereas the specific implementation must handle the message and return the response and the new state. The idea is illustrated in the following listing.
Listing 6.2 Handling messages in the server process (server_process.ex)
defmodule ServerProcess 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
❶ Invokes the callback to handle the message
Here, you expect a message in the form of a {request, caller}
tuple. The request
is data that identifies the request and is meaningful to the specific implementation. The callback function handle_call/2
takes the request payload and the current state, and it must return a {response, new_state}
tuple. The generic code can then send the response back to the caller and continue looping with the new state. There’s only one thing left to do: provide a function to issue requests to the server process.
Listing 6.3 Helper for issuing requests (server_process.ex)
defmodule ServerProcess do ... def call(server_pid, request) do send(server_pid, {request, self()}) ❶ receive do {:response, response} -> ❷ response ❸ end end end
At this point, you have the abstraction for the generic server process in place. Let’s see how it can be used.
To test the server process, you’ll implement a simple key-value store. It will be a process that can be used to store mappings between arbitrary terms.
Remember that the callback module must implement two functions: init/0
, which creates the initial state, and handle_call/2
, which handles specific requests.
Listing 6.4 Key-value store implementation (server_process.ex)
defmodule KeyValueStore do def init do %{} ❶ end def handle_call({:put, key, value}, state) do ❷ {:ok, Map.put(state, key, value)} ❷ end ❷ def handle_call({:get, key}, state) do ❸ {Map.get(state, key), state} ❸ end ❸ end
That’s all it takes to create a specific server process. Because the infinite loop and message-passing boilerplate are pushed to the generic code, the specific implementation is more concise and focused on its main task.
Take particular note of how you use a multiclause in handle_call/2
to handle different types of requests. This is the place where the specific implementation decides how to handle each request. The ServerProcess
module is generic code that blindly forward requests from client processes to the callback module.
iex(1)> pid = ServerProcess.start(KeyValueStore) iex(2)> ServerProcess.call(pid, {:put, :some_key, :some_value}) :ok iex(3)> ServerProcess.call(pid, {:get, :some_key}) :some_value
Notice how you start the process with ServerProcess.start(KeyValueStore)
. This is where you plug the specific KeyValueStore
into the generic code of ServerProcess
. All subsequent invocations of ServerProcess.call/2
will send messages to that process, which will, in turn, call KeyValueStore.handle_call/2
to perform the handling.
It’s beneficial to make clients completely oblivious to the fact that the ServerProcess
abstraction is used. This can be achieved by introducing helper functions.
Listing 6.5 Wrapping ServerProcess
function calls (server_process.ex)
defmodule KeyValueStore do def start do ServerProcess.start(KeyValueStore) end def put(pid, key, value) do ServerProcess.call(pid, {:put, key, value}) end def get(pid, key) do ServerProcess.call(pid, {:get, key}) end ... end
Clients can now use start/0
, put/3
, and get/2
to manipulate the key-value store. These functions are informally called interface functions. Clients use the interface functions of KeyValueStore
to start and interact with the process.
In contrast, init/0
and handle_call/2
are callback functions used internally by the generic code. Note that interface functions run in client processes, whereas callback functions are always invoked in the server process.
The current implementation of ServerProcess
supports only synchronous requests. Let’s expand on this and introduce support for asynchronous fire-and-forget requests, in which a client sends a message and doesn’t wait for a response.
In the current code, we use the term call for synchronous requests. For asynchronous requests, we’ll use the term cast. This is the naming convention used in OTP, so it’s good to adopt it.
Because you’re introducing the second request type, you need to change the format of messages passed between client processes and the server. This will allow you to determine the request type in the server process and handle different types of requests in different ways.
This can be as simple as including the request-type information in the tuple being passed from the client process to the server.
Listing 6.6 Including the request type in the message (server_process_cast.ex)
defmodule ServerProcess do ... def call(server_pid, request) do send(server_pid, {:call, request, self()}) ❶ ... end defp loop(callback_module, current_state) do receive do {:call, request, caller} -> ❷ ... end end ... end
❶ Tags the request message as a call
Now, you can introduce support for cast
requests. In this scenario, when the message arrives, the specific implementation handles it and returns the new state. No response is sent back to the caller, so the callback function must return only the new state.
Listing 6.7 Supporting casts in the server process (server_process_cast.ex)
defmodule ServerProcess do ... def cast(server_pid, request) do ❶ send(server_pid, {:cast, request}) ❶ end ❶ defp loop(callback_module, current_state) do receive do {:call, request, caller} -> ... {:cast, request} -> ❷ new_state = callback_module.handle_cast( request, current_state ) loop(callback_module, new_state) end end ... end
To handle a cast request, you need the callback function handle_cast/2
. This function must handle the message and return the new state. In the server loop, you then invoke this function and loop with the new state. That’s all it takes to support cast requests.
Finally, you’ll change the implementation of the key-value store to use casts. Keep in mind that a cast is a fire-and-forget type of request, so it’s not suitable for all requests. In this example, the get
request must be a call because the server process needs to respond with the value associated with a given key. In contrast, the put
request can be implemented as a cast because the client doesn’t need to wait for the response.
Listing 6.8 Implementing put
as a cast (server_process_cast.ex)
defmodule KeyValueStore do ... def put(pid, key, value) do ServerProcess.cast(pid, {:put, key, value}) ❶ end ... def handle_cast({:put, key, value}, state) do ❷ Map.put(state, key, value) end ... end
❶ Issues the put request as a cast
Now, you can try the server process:
iex(1)> pid = KeyValueStore.start() iex(2)> KeyValueStore.put(pid, :some_key, :some_value) iex(3)> KeyValueStore.get(pid, :some_key) :some_value
With a simple change in the generic implementation, you added another feature to the service process. Specific implementations can now decide whether each concrete request should be implemented as a call or a cast.
An important benefit of the generic ServerProcess
abstraction is that it lets you easily create various kinds of processes that rely on this common code. For example, in chapter 5, you developed a simple to-do server that maintains a to-do list abstraction in its internal state. This server can also be powered by the generic ServerProcess
.
This is the perfect opportunity for you to practice a bit. Take the complete code from todo_server.ex from the chapter 5 source, and save it to a different file. Then, add the last version of the ServerProcess
module to the same file. Finally, adapt the code of the TodoServer
module to work with ServerProcess
.
Once you have everything working, compare the code between the two versions. The new version of TodoServer
should be smaller and simpler, even for such a simple server process that supports only two different requests. If you get stuck, you can find the solution in the server_process_todo.ex file.
Note It’s clumsy to place multiple modules in a single file and maintain multiple copies of the ServerProcess
code in different files. In chapter 7, you’ll start using a better approach to code organization powered by the mix
tool. But for the moment, let’s stick with our current, overly simple approach.
You’re now finished implementing a basic abstraction for generic server processes. The current implementation is simple and leaves a lot of room for improvement, but it demonstrates the basic technique of generic server processes. Now, it’s time to use the full-blown OTP abstraction for generic server processes: GenServer
.
When it comes to production-ready code, it doesn’t make much sense to build and use the manually baked ServerProcess
abstraction. That’s because Elixir ships with a much better support for generic server processes, called GenServer
. In addition to being much more feature rich than ServerProcess
, GenServer
also handles several edge cases and is battle tested in production in complex concurrent systems.
Some of the compelling features provided by GenServer
include the following:
Note that there’s no special magic behind GenServer
. Its code relies on concurrency primitives explained in chapter 5 and fault-tolerance features explained in chapter 9. After all, GenServer
is implemented in plain Erlang and Elixir. The heavy lifting is done in the :gen_server
module, which is included in the Erlang standard library. Some additional wrapping is performed in the Elixir standard library, in the GenServer
module.
In this section, you’ll learn how to build your server processes with GenServer
. But first, let’s examine the concept of OTP behaviours.
Note Note the British spelling of the word behaviour : this is the preferred spelling both in OTP code and official documentation. This book uses the British spelling to specifically denote an OTP behaviour but retains the American spelling (behavior) for all other purposes.
In Erlang terminology, a behaviour is generic code that implements a common pattern. The generic logic is exposed through the behaviour module, and you can plug into it by implementing a corresponding callback module. The callback module must satisfy a contract defined by the behaviour, meaning it must implement and export a set of functions. The behaviour module then calls into these functions, allowing you to provide your own specialization of the generic code.
This is exactly what ServerProcess
does. It powers a generic server process, requiring specific implementations to provide the callback module that implements the init/0
, handle_call/2
, and handle_cast/2
functions. ServerProcess
is a simple example of a behaviour.
It’s even possible to specify the behaviour contract and verify that the callback module implements required functions during compilation. For details, see the official documentation (https://hexdocs.pm/elixir/Module.xhtml#module-behaviour).
The Erlang standard library includes the following OTP behaviours:
gen_server
—Generic implementation of a stateful server process
supervisor
—Provides error handling and recovery in concurrent systems
application
—Generic implementation of components and libraries
gen_statem
—Runs a finite state machine in a stateful server process
Elixir provides its own wrappers for the most frequently used behaviours via the modules GenServer
, Supervisor
, and Application
. This book focuses on these behaviours. The GenServer
behaviour receives detailed treatment in this chapter and chapter 7, Supervisor
is discussed in chapters 8 and 9, and Application
is presented in chapter 11.
The remaining behaviours, although useful, are used less often and won’t be discussed in this book. Once you get a grip on GenServer
and Supervisor
, you should be able to research other behaviours on your own and use them when the need arises. You can find more about gen_event
and gen_statem
in the Erlang documentation (https://erlang.org/doc/design_principles/des_princ.xhtml).
Using GenServer
is roughly similar to using ServerProcess
. There are some differences in the format of the returned values, but the basic idea is the same.
The GenServer
behaviour defines eight callback functions, but frequently, you’ll need only a subset of those. You can get some sensible default implementations of all required callback functions if you use
the GenServer
module:
iex(1)> defmodule KeyValueStore do use GenServer end
The use
macro is a language feature we haven’t previously discussed. During compilation, when this instruction is encountered, the specific macro from the GenServer
module is invoked. That macro then injects several functions into the calling module (KeyValueStore
, in this case). You can verify this in the shell:
iex(2)> KeyValueStore.__info__(:functions) [child_spec: 1, code_change: 3, handle_call: 3, handle_cast: 2, handle_info: 2, init: 1, terminate: 2]
Here you use the __info__/1
function that’s automatically added to each Elixir module during compilation. It lists all exported functions of a module (except __info__/1
).
As you can see in the output, many functions are automatically included in the module due to use GenServer
. These are all callback functions that need to be implemented for you to plug into the GenServer
behaviour.
Of course, you can then override the default implementation of each function, as required. If you define a function of the same name and arity in your module, it will overwrite the default implementation you get through use
.
At this point, you can plug your callback module into the behaviour. To start the process, use the GenServer.start/2
function:
iex(3)> GenServer.start(KeyValueStore, nil) {:ok, #PID<0.51.0>}
This works roughly like ServerProcess
. The server process is started, and the behaviour uses KeyValueStore
as the callback module. The second argument of GenServer.start/2
is a custom parameter that’s passed to the process during its initialization. For the moment, you don’t need this, so you send the nil
value. Finally, notice that the result of GenServer.start/2
is a tuple of the form {:ok, pid}
.
Now you can convert the KeyValueStore
to work with GenServer
. To do this, you need to implement three callbacks: init/1
, handle_cast/2
, and handle_call/3
.
Listing 6.9 Implementing GenServer
callbacks (key_value_gen_server.ex)
defmodule KeyValueStore do use GenServer def init(_) do {:ok, %{}} end def handle_cast({:put, key, value}, state) do {:noreply, Map.put(state, key, value)} end def handle_call({:get, key}, _, state) do {:reply, Map.get(state, key), state} end end
These callbacks work similarly to the ones in ServerProcess
, with a couple of differences:
init/1
accepts one argument. This is the second argument provided to GenServer.start/2
, and you can use it to pass data to the server process while starting it.
The result of init/1
must be in the format {:ok, initial_state}
.
handle_cast/2
accepts the request and the state and should return the result in the format {:noreply, new_state}
.
handle_call/3
takes the request, caller information, and state. It should return the result in the following format {:reply, response, new_state}
.
The second argument to handle_call/3
is a tuple that contains the request ID (used internally by the GenServer
behaviour) and the PID of the caller. This information is, in most cases, not needed, so in this example, you ignore it.
With these callbacks in place, the only things missing are interface functions. To interact with a GenServer
process, you can use functions from the GenServer
module. In particular, you can use GenServer.start/2
to start the process and GenServer.cast/2
and GenServer.call/2
to issue requests. The code is shown in the next listing.
Listing 6.10 Adding interface functions (key_value_gen_server.ex)
defmodule KeyValueStore do use GenServer def start do GenServer.start(KeyValueStore, nil) end def put(pid, key, value) do GenServer.cast(pid, {:put, key, value}) end def get(pid, key) do GenServer.call(pid, {:get, key}) end ... end
That’s it! With only a few changes, you’ve moved from a basic ServerProcess
to a full-blown GenServer
. Let’s test the server:
iex(1)> {:ok, pid} = KeyValueStore.start() iex(2)> KeyValueStore.put(pid, :some_key, :some_value) iex(3)> KeyValueStore.get(pid, :some_key) :some_value
There are many differences between ServerProcess
and GenServer
, but a couple of points deserve special mention.
First, GenServer.start/2
returns only after the init/1
callback has finished in the server process. Consequently, the client process that starts the server is blocked until the server process is initialized.
Second, GenServer.call/2
doesn’t wait indefinitely for a response. By default, if the response message doesn’t arrive in 5 seconds, an error is raised in the client process. You can alter this by using GenServer.call(pid, request, timeout)
, where the timeout is given in milliseconds. In addition, if the receiver process happens to terminate while you’re waiting for the response, GenServer
detects it and raises a corresponding error in the caller process.
Messages sent to the server process via GenServer.call
and GenServer.cast
contain more than just a request payload. Those functions include additional data in the message sent to the server process. This is something you did in the ServerProcess
example in section 6.1:
defmodule ServerProcess do ... def call(server_pid, request) do send(server_pid, {:call, request, self()}) ❶ ... end def cast(server_pid, request) do send(server_pid, {:cast, request}) ❷ end ... defp loop(callback_module, current_state) do receive do {:call, request, caller} -> ❸ ... {:cast, request} -> ❹ ... end end ... end
❸ Special handling of a call message
❹ Special handling of a cast message
Notice that you don’t send the plain request
payload to the server process; you include additional data, such as the request type and the caller, for call requests.
GenServer
uses a similar approach, using :"$gen_cast"
and :"$gen_call"
atoms to decorate cast and call messages. You don’t need to worry about the exact format of those messages, but it’s important to understand that GenServer
internally uses particular message formats and handles those messages in a specific way.
Occasionally, you may need to handle messages that aren’t specific to GenServer
. For example, imagine you need to do a periodic cleanup of the server process state. You can use the Erlang function :timer.send_interval/2
, which periodically sends a message to the caller process. Because this message isn’t a GenServer
-specific message, it’s not treated as a cast or call. Instead, for such plain messages, GenServer
calls the handle_info/2
callback, giving you a chance to do something with the message.
Here’s a sketch of this technique:
iex(1)> defmodule KeyValueStore do use GenServer def init(_) do :timer.send_interval(5000, :cleanup) ❶ {:ok, %{}} end def handle_info(:cleanup, state) do ❷ IO.puts "performing cleanup..." {:noreply, state} end end iex(2)> GenServer.start(KeyValueStore, nil) performing cleanup... ❸ performing cleanup... performing cleanup...
❶ Sets up periodic message sending
❷ Handles the plain :cleanup message
During process initialization, you ensure a :cleanup message
is sent to the process every 5 seconds. This message is handled in the handle_info/2
callback, which essentially works like handle_cast/2
, returning the result as {:noreply, new_state}
.
There are several other features and subtleties I haven’t mentioned in this basic introduction to GenServer
. You’ll learn about some of them elsewhere in this book, but you should definitely take the time to look over the documentation for the GenServer
module (https://hexdocs.pm/elixir/GenServer.xhtml) and its Erlang foundation (https://erlang.org/doc/man/gen_server.xhtml). A couple of points still deserve special mention.
One problem with the callback mechanism is that it’s easy to make a subtle mistake when defining a callback function. Consider the following example:
iex(1)> defmodule EchoServer do use GenServer def handle_call(some_request, server_state) do {:reply, some_request, server_state} end end
Here, you have a simple echo server, which handles every call request by sending the request back to the client. Try it out:
iex(2)> {:ok, pid} = GenServer.start(EchoServer, nil) {:ok, #PID<0.96.0>} iex(3)> GenServer.call(pid, :some_call) ** (exit) exited in: GenServer.call(#PID<0.96.0>, :some_call, 5000) ** (EXIT) an exception was raised: ** (RuntimeError) attempted to call GenServer #PID<0.96.0> but no handle_call/3 clause was provided
Issuing a call caused the server to crash with an error that no handle_call/3
clause is provided, although the clause is listed in the module. What happened? If you look closely at the definition of EchoServer
, you’ll see that you defined handle_call/2
, while GenServer
requires handle_call/3
.
You can get a compile-time warning here if you tell the compiler that the function being defined is supposed to satisfy a contract by some behaviour. To do this, you need to provide the @impl
module attribute immediately before the first clause of the callback function:
iex(1)> defmodule EchoServer do
use GenServer
@impl GenServer ❶
def handle_call(some_request, server_state) do
{:reply, some_request, server_state}
end
end
❶ Indicates an upcoming definition of a callback function
The @impl GenServer
tells the compiler that the function about to be defined is a callback function for the GenServer
behaviour. As soon as you execute this expression in the shell, you’ll get a warning:
warning: got "@impl GenServer" for function handle_call/2 but this behaviour does not specify such callback.
The compiler tells you that GenServer
doesn’t deal with handle_call/2
, so you already get a hint that something is wrong during compilation. It’s a good practice to always specify the @impl
attribute for every callback function you define in your modules.
Recall from chapter 5 that a process can be registered under a local name (an atom), where local means the name is registered only in the currently running BEAM instance. This allows you to create a singleton process you can access by name without needing to know its PID.
Local registration is an important feature because it supports patterns of fault tolerance and distributed systems. You’ll see exactly how this works in later chapters, but it’s worth mentioning that you can provide the process name as an option to GenServer.start
:
GenServer.start(
CallbackModule,
init_param,
name: :some_name ❶
)
❶ Registers the process under a name
You can then issue calls and casts using the name:
GenServer.call(:some_name, ...) GenServer.cast(:some_name, ...)
The most frequent approach is to use the same name as the module name. As explained in section 2.4.2, module names are atoms, so you can safely pass them as the :name
option. Here’s a sketch of this approach:
defmodule KeyValueStore do def start() do GenServer.start(KeyValueStore, nil, name: KeyValueStore) ❶ end def put(key, value) do GenServer.cast(KeyValueStore, {:put, key, value}) ❷ end ... end
❶ Registers the server process
❷ Sends a request to the registered process
Notice how KeyValueStore.put
now doesn’t need to take the PID. It will simply issue a request to the registered process.
You can also replace KeyValueStore
with the special form __MODULE__
. During compilation, __MODULE__
is replaced with the name of the module where the code resides:
defmodule KeyValueStore do def start() do GenServer.start(__MODULE__, nil, name: __MODULE__) ❶ end def put(key, value) do GenServer.cast(__MODULE__, {:put, key, value}) ❷ end ... end
❶ Registers the server process
❷ Sends a request to the registered process
After compilation, this code is equivalent to the previous version, but some future refactoring is made easier. If, for example, you rename KeyValueStore
as KeyValue .Store
, you only need to do it in one place in the module.
Different callbacks can return various types of responses. So far, you’ve seen the most common cases:
There are additional possibilities, with the most important being the option to stop the server process.
In init/1
, you can decide against starting the server. In this case, you can either return {:stop, reason}
or :ignore
. In both cases, the server won’t proceed with the loop and will instead terminate.
If init/1
returns {:stop, reason}
, the result of start/2
will be {:error, reason}
. In contrast, if init/1
returns :ignore
, the result of start/2
will also be :ignore
. The difference between these two return values is their intention. You should opt for {:stop, reason}
when you can’t proceed further due to some error. In contrast, :ignore
should be used when stopping the server is the normal course of action.
Returning {:stop, reason, new_state}
from handle_*
callbacks causes GenServer
to stop the server process. If the termination is part of the standard workflow, you should use the atom :normal
as the stoppage reason. If you’re in handle_call/3
and also need to respond to the caller before terminating, you can return {:stop, reason, response, new_state}
.
You may wonder why you need to return a new state if you’re terminating the process. This is because just before the termination, GenServer
calls the callback function terminate/2
, sending it the termination reason and the final state of the process. This can be useful if you need to perform cleanup.
Finally, you can also stop the server process by invoking GenServer.stop/3
from the client process. This invocation will issue a synchronous request to the server. The behaviour will handle the stop request itself by stopping the server process.
It’s important to always be aware of how GenServer
-powered processes tick and where (in which process) various functions are executed. Let’s do a quick recap by looking at figure 6.1, which shows the life cycle of a typical server process.
Figure 6.1 Life cycle of a GenServer
-powered process
A client process starts the server by calling GenServer.start
and providing the callback module (1). This creates the new server process, which is powered by the GenServer
behaviour.
Requests can be issued by client processes using various GenServer
functions or plain send
. When a message is received, GenServer
invokes callback functions to handle it. Therefore, callback functions are always executed in the server process.
The process state is maintained in the GenServer
loop but is defined and manipulated by the callback functions. It starts with init/1
, which defines the initial state that’s then passed to subsequent handle_*
callbacks (2). Each of these callbacks receives the current state and must return its new version, which is used by the GenServer
loop in place of the old one.
For various reasons, once you start building production systems, you should avoid using plain processes started with spawn
. Instead, all of your processes should be so-called OTP-compliant processes. Such processes adhere to OTP conventions; they can be used in supervision trees (described in chapter 9); and errors in those processes are logged with more details.
All processes powered by OTP behaviours, such as GenServer
and Supervisor
, are OTP-compliant. Elixir also includes other modules that can be used to run OTP-compliant processes. For example, the Task
module (https://hexdocs.pm/elixir/Task.xhtml) is perfect to run one-off jobs that process some input and then stop. The Agent
module (https://hexdocs.pm/elixir/Agent.xhtml) is a simpler (but less powerful) alternative to GenServer
-based processes and is appropriate if the sole purpose of the process is to manage and expose state. Both Task
and Agent
are discussed in chapter 10.
In addition, there are various other OTP-compliant abstractions available via third-party libraries. For example, GenStage
(https://hexdocs.pm/gen_stage) can be used for back pressure and load control. The Phoenix.Channel
module (https://hexdocs.pm/phoenix/Phoenix.Channel.xhtml), which is part of the Phoenix web framework (https://phoenixframework.org), is used to facilitate bidirectional communication between a client and a web server over protocols such as WebSocket or HTTP.
There isn’t enough space in this book to treat every possible OTP-compliant abstraction, so you’ll need to do some research on your own. But it’s worth pointing out that most such abstractions follow the ideas of GenServer
. Except for the Task
module, all of the OTP abstractions mentioned in this section are internally implemented on top of GenServer
. Therefore, in my personal opinion, GenServer
is likely the most important part of OTP. If you properly understand the principles of GenServer
, most other abstractions should be much easier to grasp.
Let’s wrap up this chapter with a simple, but important, exercise. For practice, try to change the to-do server implemented earlier in this chapter to work with the GenServer
behaviour. This should be a straightforward task, but if you get stuck, the solution is in the todo_server.ex file.
Be sure to either finish this exercise or analyze and understand the solution, because in future chapters, you’ll gradually expand on this simple server process and build a highly concurrent distributed system.
A generic server process is an abstraction that implements tasks common to any kind of server process, such as recursion-powered looping and message passing.
A generic server process can be implemented as a behaviour. A behaviour drives the process, whereas specific implementations can plug into the behaviour via callback modules.
The behaviour invokes callback functions when the specific implementation needs to make a decision.
GenServer
is a behaviour that implements a generic server process.
A callback module for GenServer
must implement various functions. The most frequently used of these are init/1
, handle_cast/2
, handle_call/3
, and handle_info/2
.
You can interact with a GenServer
process with the GenServer
module.
Two types of requests can be issued to a server process: calls and casts.
A cast is a fire-and-forget type of request—a caller sends a message and immediately moves on to do something else.
A call is a synchronous send-and-respond request—a caller sends a message and waits until the response arrives, the timeout occurs, or the server crashes.