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.