Creating LFE Servers with OTP, Part II
In the last post, we went on a whirlwind tour of gen_server
's basic
functionality: we created a callback module which embodied our logic; we
created a server module that was responsible for setting up the loop;, and we
added an API to wrap gen_server:cast
and gen_server:call
functions that
passed messages to our callback logic. In this post we're going to follow up on
that work:
- Update our code to use OTP community best practices
- Add support for stopping the server.
- Improve the support for handling unexpected messages.
LFE OTP Tutorial Series
- Introducing the LFE OTP Tutorials
- What is OTP?
- Prelude to OTP
- Creating LFE Servers with OTP, Part I
- Creating LFE Servers with OTP, Part II
- Distributed LFE
You can leave feedback for the LFE OTP tutorials here.
In This Post
- Requirements, Assumptions, and Code
- Best Practices
- Unified Code
- Exports
- All Callbacks
- Stopping a
gen_server
- Expecting the Unexpected
- Full Source Code
- Learning More About
gen_server
- Up Next
Requirements, Assumptions, and Code
Before reading this tutorial, be sure you should have read the ones preceding it in this series. For a list of what you need to have installed before working through the examples as well as getting the source code for these tutorials, please see the post Prelude to OTP.
In the last post, we compiled all the code and started the LFE REPL with the following command:
$ cd tut01
$ make repl
Best Practices
The next few sections will cover some of the best practices that we glossed
over or completely ignored in the last post. Our intent was to convey the
core concepts of gen_server
without drowning the new OTP developer in
a sea of details. Now it's time to complete your gen_server
education
and tell you the rest of the story. Ready to swim?
Unified Code
Even though OTP provides the developer with the ability to define the callbacks
in a separate module like we did in the last post, in practice this feature is
not generally utilized. Instead, both the callback logic and the server code are
kept in the same module. This makes it possible for the two aspects of gen_server
to share private functions and it makes it easier to refer to each other (no need to
make calls to another module).
It may seem a bit awkward at first, though, since you'll be using the same name for
the gen_server
and for the callback module (namely, (MODULE)
), but you'll
get used to it quickly enough.
The tut01/src
directory which holds the source code for this post (and the previous
one) has a combined module, tut01.lfe
holding the functionality we previously
defined in tut01-server.lfe
and tut01-callback.lfe
. We'll give a full
listing at the end of this post.
Explicit Exports
The next thing we need to fix from the last tutorial is the lazy use of (export all)
.
It is much better to declare exactly what you want exported for public use. The explicit
exporting of public functions is part of the (self-) documentation for your module.
When opening your module in an editor, this is going to be less helpful:
(defmodule tut01-server
(behaviour gen_server)
(export all))
Than this:
(defmodule tut01
(behaviour gen_server)
(export
;; gen_server implementation
(start 0)
(stop 0)
;; callback implementation
(init 1)
(handle_call 3)
(handle_cast 2)
(handle_info 2)
(terminate 2)
(code_change 3)
;; server API
(inc 0)
(dec 0)
(amount? 0)))
All Callbacks
The last best practice we're going to look at now is the inclusion of all callbacks. When you compile an LFE module that declares an OTP behaviour, it doesn't complain if you leave out a required function. It successfully compiles and will run just fine. However, when you do this you are not abiding by the contract with the OTP world. This can have the practical result of causing unexpected bugs and/or breakages in code, especially in the code of your users who would be expecting your application to respect the OTP contract.
So what do we need to add to our new, unified gen_server
and callback module that
we left out in the last post? Here are the additional required gen_server
functions:
handle_info/2
terminate/2
code_change/3
The final, optional gen_server
callback function is format_status/2
, but we'll
discuss that in a future post when we touch on the topic of monitoring nodes.
The functions handle_info
and terminate
will be the topic of the next sections,
so let's stub out a quick code_change
implementation:
(defun code_change (_old-version state _extra)
`#(ok ,state))
A future blog post will dive into the topic of hot-loading new code into a running
server (zero downtime!), and at that point we'll provide a real implementation
of code_change
. For now, what we have above will be a placeholder.
API Update Reprise: Stopping a gen_server
In the last post we showed how easy it was to update an API (and what was needed in
order to do so). We're going to return to this topic, but with a twist. We're going
to add a stop
function to our API, but stopping a service is a little more
involved than a regular API addition. We'll explain as we go.
Here's the new API function:
(defun stop ()
(gen_server:call (server-name) 'stop))
That’s the easy bit. Now let’s add support for this new stop
message to our
handle_call
callback function:
(defun handle_call
(('amount _caller state-data)
`#(reply ,state-data ,state-data))
(('stop _caller state-data)
`#(stop shutdown ok state-data))
((message _caller state-data)
`#(reply ,(unknown-command) ,state-data)))
Notice that we've got a new return tuple: it doesn't start with reply
or noreply
.
Instead, it sends the stop
message.
If we tried to run our server with just this
change to the callback, we would get an error after calling our new (tut01:stop)
API function:
=ERROR REPORT==== 25-May-2015::19:27:16 ===
** Generic server tut01 terminating
** Last message in was stop
** When Server state == 'state-data'
** Reason for termination ==
** {'function not exported',
[{'tut01-callback',terminate,...},
{gen_server,try_terminate,...},
{gen_server,terminate,7,...},
{gen_server,handle_msg,5,...},
{proc_lib,init_p_do_apply,3,...}]}
You will see that error message when you haven’t defined the terminate
callback function. Here’s a quick one. Let's fix that:
(defun terminate (_reason _state-data)
'ok)
Now when we stop our server using our new API, we have success:
> (tut01:start)
#(ok <0.35.0>)
> (tut01:stop)
ok
And we can demonstrate that it’s really stopped by trying to call our
amount?
API function:
> (tut01:amount?)
exception exit: #(noproc #(gen_server call (tut01 amount)))
in gen_server:call/2 (gen_server.erl, line 182)
Expecting the Unexpected
In the last post, we added a quick catch-all pattern to our handle_call
callback
to provide a user with feedback whenever they attempt to call an API that's not
defined. This is rather fragile, since there are different types of messages which
can be sent to the gen_server
, many of which won't be done by a human user.
Here's an example stray message:
> (! (whereis 'tut01) 'bingo)
bingo
Which causes the termination of our server:
=ERROR REPORT==== 30-May-2015::20:38:25 ===
** Generic server tut01 terminating
** Last message in was bingo
** When Server state == 0
** Reason for termination ==
** {'function not exported',
[{tut01,handle_info,[bingo,0],[]},
{gen_server,try_dispatch,4,[{file,"gen_server.erl"},{line,593}]},
{gen_server,handle_msg,5,[{file,"gen_server.erl"},{line,659}]},
{proc_lib,init_p_do_apply,3,[{file,"proc_lib.erl"},{line,237}]}]}
The callback that gen_server
will use to handle undefined messages is handle_info
.
Let's create an implementation for this callback which is less fragile that our
original:
(defun handle_info
((`#(EXIT ,_pid normal) state-data)
`#(noreply ,state-data))
((`#(EXIT ,pid ,reason) state-data)
(io:format "Process ~p exited! (Reason: ~p)~n" `(,pid ,reason))
`#(noreply ,state-data))
((_msg state-data)
`#(noreply ,state-data)))
Let's play gen_server
bingo again:
> (tut01:start)
#(ok <0.35.0>)
> (! (whereis 'tut01) 'bingo)
bingo
So much nicer!
Full Source Code
With all these changes, we have some new source code to enjoy:
(defmodule tut01
(behaviour gen_server)
(export
;; gen_server implementation
(start 0)
(stop 0)
;; callback implementation
(init 1)
(handle_call 3)
(handle_cast 2)
(handle_info 2)
(terminate 2)
(code_change 3)
;; server API
(inc 0)
(dec 0)
(amount? 0)))
;;; config functions
(defun server-name () (MODULE))
(defun callback-module () (MODULE))
(defun initial-state () 0)
(defun genserver-opts () '())
(defun register-name () `#(local ,(server-name)))
(defun unknown-command () #(error "Unknown command."))
;;; gen_server implementation
(defun start ()
(gen_server:start (register-name)
(callback-module)
(initial-state)
(genserver-opts)))
(defun stop ()
(gen_server:call (server-name) 'stop))
;;; callback implementation
(defun init (initial-state)
`#(ok ,initial-state))
(defun handle_cast
(('inc state-data)
`#(noreply ,(+ 1 state-data)))
(('dec state-data)
`#(noreply ,(- state-data 1))))
(defun handle_call
(('amount _caller state-data)
`#(reply ,state-data ,state-data))
(('stop _caller state-data)
`#(stop shutdown ok state-data))
((message _caller state-data)
`#(reply ,(unknown-command) ,state-data)))
(defun handle_info
((`#(EXIT ,_pid normal) state-data)
`#(noreply ,state-data))
((`#(EXIT ,pid ,reason) state-data)
(io:format "Process ~p exited! (Reason: ~p)~n" `(,pid ,reason))
`#(noreply ,state-data))
((_msg state-data)
`#(noreply ,state-data)))
(defun terminate (_reason _state-data)
'ok)
(defun code_change (_old-version state _extra)
`#(ok ,state))
;;; our server API
(defun inc ()
(gen_server:cast (server-name) 'inc))
(defun dec ()
(gen_server:cast (server-name) 'dec))
(defun amount? ()
(gen_server:call (server-name) 'amount))
Ah, look at all those beautiful parentheses :-) 1
Learning More About gen_server
There’s a lot of information we didn’t cover in this tutorial, however it’s
enough to get started writing simple OTP servers in LFE. If you’d like to learn
more, one of the newest books out there on the topic has been recently
published by O’Reilly:
Designing for Scalability with Erlang/OTP.
The chapter on gen_server
covers this material in much more detail,
including timeouts, deadlocks, hibernating, and custom global registries.
Another good resource, once you get up to speed, is the Erlang documentation (and here). Often overlooked, it's actually really good and will be a constant companion for you any time you need to do something with OTP that you haven't tried before.
In future posts in this series, we will be covering bits we've left out of this tutorial, namely:
code_change
- supporting hot-loading of code in a running systemformat_status
- providing custom status data for a running server
Up Next
Before we tackle any other behaviours, we’re going to explore distributed generic servers: running our code on multiple cores and multiple machines.
Footnotes
-
You might have noticed that we put the
stop
API function in withstart
. Even thoughstop
is not defined forgen_server
, we still consider it a "server management" function and thus place it with its peer,start
. ↩