June 9th, 2008

Erlang: A Generic Server Tutorial

One of the benefits of working with Erlang is that it was designed with real-world applications in mind. This is reflected in OTP, or Open Telecommunications Platform, a set of standard libraries that come with the default Erlang VM.

Erlang/OTP implements in a generic way lots of networking paradigms, including finite state machines (gen_fsm), event handling (gen_event), and client/server interaction (gen_server). We’re going to cover on the last library, gen_server, or Erlang/OTP’s generic server library.

The Client/Server Model

The client/server model is based around many clients connecting to a single, central server. The clients can send and receive message from the server while the server maintains a global state.

Here’s a picture.

A common instance where the client/server model makes sense is when you have some resource you want to distribute among several people. The server controls access and allocation of the resource and the clients consume it.

The Code

Code speaks louder than words, so without further ado here is a simple server server that simulates a library. People can check out and return books from the library, but there’s only one copy of each book.

-module(library).
-author(‘Jesse E.I. Farmer <jesse@20bits.com>’).
-behaviour(gen_server).

-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).

-export([start/0, checkout/2, lookup/1, return/1]).

% These are all wrappers for calls to the server
start() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
checkout(Who, Book) -> gen_server:call(?MODULE, {checkout, Who, Book})
lookup(Book) -> gen_server:call(?MODULE, {lookup, Book}).
return(Book) -> gen_server:call(?MODULE, {return, Book}).

% This is called when a connection is made to the server
init([]) ->
    Library = dict:new(),
    {ok, Library}.

% handle_call is invoked in response to gen_server:call
handle_call({checkout, Who, Book}, _From, Library) ->
    Response = case dict:is_key(Book, Library) of
        true ->
            NewLibrary = Library,
            {already_checked_out, Book};
        false ->
            NewLibrary = dict:append(Book, Who, Library),
            ok
    end,
    {reply, Response, NewLibrary};

handle_call({lookup, Book}, _From, Library) ->
    Response = case dict:is_key(Book, Library) of
        true ->
            {who, lists:nth(1, dict:fetch(Book, Library))};
        false ->
            {not_checked_out, Book}
    end,
    {reply, Response, Library};

handle_call({return, Book}, _From, Library) ->
    NewLibrary = dict:erase(Book, Library),
    {reply, ok, NewLibrary};

handle_call(_Message, _From, Library) ->
    {reply, error, Library}.

% We get compile warnings from gen_server unless we define these
handle_cast(_Message, Library) -> {noreply, Library}.
handle_info(_Message, Library) -> {noreply, Library}.
terminate(_Reason, _Library) -> ok.
code_change(_OldVersion, Library, _Extra) -> {ok, Library}.

Breaking It Down

The first line of interest is -behaviour(gen_server). This tells Erlang that we’ll be using gen_server module for our behavior.

Next we implement wrappers for server calls. We start the library server by calling library:start/0, which in turn calls gen_server:start_link/4.

Whatever we pass to start_link/4 will be passed to init/1 later, which is the callback that handles connection events. In our case we just want to create a new dictionary to store which books have been checked out.

Once we’ve started the server we want to be able to check out books, see if a book has been checked out, and return books. We implement wrappers to handle these functions, each of which invokes gen_server:call/2.

gen_server:call is used for synchronous communication between the client and the server. That is, it is used when the server expects a response. These calls are handled by handle_call (big surprise, huh?).

All of the meat is in the handle_call definitions. As you can see the server understands three messages: checkout, lookup, and return. We have one definition of handle_call for each possible message and a default action that returns an error when it receives a message it doesn’t understand.

Here’s an example of how you’d actually use the library server. All of the commands are executed in the Erlang shell, erl.

1> c(library).
{ok,library}
2> library:start().
{ok,<0.39.0>}
3> library:checkout(jesse, "American Creation").
ok
4> library:lookup("American Creation").
{who,jesse}
5> library:checkout(james, "American Creation").
{already_checked_out,"American Creation"}
6> library:return("American Creation").
ok
7> library:checkout(james, "American Creation").
ok

Other Goodies and Caveats

Writing code with gen_server isn’t all academic. There are real benefits.

Abstraction

The greatest benefit of gen_server is the abstraction it provides. By encapsulating the essence of the client/server model we can focus on the business logic rather than low-level event management.

More importantly, however, it abstracts away the protocol. The code behind the scenes can change without affecting the client/server behavior.

Supervision

Although we don’t make use of it here, gen_server supports supervision behaviors. If a call throws an exception the server can capture it and restart the appropriate section of code. This is handled using handle_info. This becomes more important if the server is spawning additional processes.

Code Swapping

We don’t make use of this either, but gen_server supports hot code swapping using the code_changed callback. This is one place where Erlang really shines and gen_server carries it through to the client/server model.

Caveats

It’s not all awesome, though. It’s surprisingly tricky to write gen_server code that handles TCP/IP connections. I’ll give an example of mixing networking and gen_server in a future article, but there are all sorts of control and blocking issues that have to be dealt with.

Leave a comment if you have any cool gen_server examples out there.

  • You wrote "...so without further adieu..."

    Hmmm .. you probably don't want the French for 'goodbye', but the English 'ado', as in fuss, trouble, or bother.
  • Pretty boring example. I've been learning erlang and working on a gen_server that uses the "distributed applications" feature and replicated mnesia. I can kill one of the servers and the other takes over with the same state.
  • Mr. Design,

    What can I say? Everyone's a critic.

    This tutorial was aimed at people who don't know Erlang that well. It took me a good two or three days to suss out how to use gen_server properly. The current documentation is extremely dense.

    Also, I'm going to be following up with more "interesting" (read: complex) examples, so keep your eyes peeled.
  • William Allen
    Mr/Ms "Web Design Company" should perhaps learn how to appreciate the effort involved in creating such a "boring example".
    As I found it both informative and well written; what else would one want from a "Tutorial". :)
  • Thank you for the example...

    If eveyone was doint at least that...

    Bye. Gostovanje na spletnem strežniku
  • Vita
    Thanks for your article, Jesse.
    I've started learning Erlang recently and i find your articles very useful.
  • I'm just starting to learn Erlang and Your tutorial is perfect for me! Big thanks. And for mister web design I have a question where is the link to his "fantastic" tutorial about what hi described. I've missed it.
  • Thanks nice site,It is big help to start.
blog comments powered by Disqus