When we wrote our Fibonacci server in the previous chapter,, we had to do all the message handling ourselves. It wasn’t difficult, but it was tedious. Our scheduler also had to keep track of three pieces of state information: the queue of numbers to process, the results generated so far, and the list of active PIDs.
Most servers have a similar set of needs, so OTP provides libraries that do all the low-level work for us.
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. For example, 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.
Think back to our recursive Fibonacci code. Where did it keep all the intermediate results as it worked? It passed them to itself, recursively, as parameters. In fact, all three of its parameters were used for state information.
Now think about servers. They use recursion to loop, handling one request on each call. So they can also pass state to themselves as a parameter in this recursive call. And that’s one of the things OTP manages for us. Our handler functions get passed the current state (as their last parameter), and they return (among other things) a potentially updated state. Whatever state a function returns is the state that will be passed to the next request handler.
Let’s write what is possibly the simplest OTP server. You pass it a number when you start it up, and that becomes the current state of the server. When you call it with a :next_number request, it returns that current state to the caller, and at the same time increments the state, ready for the next call. Basically, each time you call it you get an updated sequence number.
Start by creating a new mix project in your work directory. We’ll call it sequence.
| $ mix new sequence |
| * creating README.md |
| * creating .formatter.exs |
| * creating .gitignore |
| * creating mix.exs |
| * creating config |
| * creating config/config.exs |
| * creating lib |
| * creating lib/sequence.ex |
| * creating test |
| * creating test/test_helper.exs |
| * creating test/sequence_test.exs |
Now we’ll create Sequence.Server, our server module. Move into the sequence directory, and create a subdirectory under lib/ also called sequence.
| $ cd sequence |
| $ mkdir lib/sequence |
Add the file server.ex to lib/sequence/:
1: | defmodule Sequence.Server do |
- | use GenServer |
- | |
- | def init(initial_number) do |
5: | { :ok, initial_number } |
- | end |
- | |
- | def handle_call(:next_number, _from, current_number) do |
- | {:reply, current_number, current_number + 1} |
10: | end |
- | end |
The first thing to note is line 2. The use line effectively adds the OTP GenServer behavior to our module. This is what lets it handle all the callbacks. It also means we don’t have to define every callback in our module—the behavior defines defaults for all but one of them.
The exception is the init/1 function, defined on line 4. You can think of init as being like the constructor in an object-oriented language: A constructor takes values and creates the object’s initial state, and init takes some initial value and uses it to construct the state of the server. This state is returned as the second element of the {:ok, state} tuple. In our case, we use the init function to set the initial value of our counter.
When a client calls our server, GenServer invokes its handle_call function. This function receives three parameters:
Your implementation of the function should perform the actions associated with the first parameter, and may update the state (the third parameter). When the handle_call function exits, it must return the state (updated or not).
The initial state of a GenServer is set by the return value of the init function.
Our implementation is simple: we return a tuple to OTP.
| { :reply, current_number, current_number+1 } |
The reply element tells OTP to reply to the client, passing back the value that is the second element. Finally, 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.
We can play with our server in IEx. Open it in the project’s main directory, remembering the -S mix option.
| $ 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) |
| 102 |
We’re using two functions from the Elixir GenServer module. The start_link function behaves like the spawn_link function we used in the previous chapter. It asks GenServer to start a new process and link to us (so we’ll get notifications if it fails). We pass in the module to run as a server: the initial state (100 in this case). We could also pass GenServer options as a third parameter, but the defaults work fine here.
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.
In our case, 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. 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.
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. We could define the handler as
| def handle_call({:set_number, new_number}, _from, _current_number) do |
| { :reply, new_number, new_number } |
| end |
and call it with
| iex> GenServer.call(pid, {:set_number, 999}) |
| 999 |
Similarly, a handler can return multiple values by packaging them into a tuple or list.
| def handle_call({:factors, number}, _, _) do |
| { :reply, { :factors_of, number, factors(number)}, [] } |
| end |