Note, this post is written against the 2.0.1 version of gen_server
Erlang comes with a rich set of small concurrency primitives to make handling and manipulating state easier. The most generic of the frameworks is the gen_server
which is also the most commonly used. A gen_server
provides a way to control state over multiple requests. It serializes operations and handles both synchronous and asynchronous communication with clients. The strength of a gen_server
is the ability to create multiple, lightweight, servers inside an application where each operation inside of it runs in serial but individually the gen_server
s run concurrently.
While it is not possible to provide all of the Erlang semantics in Ocaml, we can create something roughly analogous. We can also get some properties that Erlang can not give us. In particular, the implementation of gen_server
provided here:
- Does not have the concept of a process or a process id. A
gen_server
is an abstract type that is parameterized by a message type. - Uses queues to communicate messages between clients and servers.
gen_server
s are typesafe, only messages that they can handle can be sent to them.- You can only communicate with
gen_server
s in your own process, there is no concept of location ignorance. - Only provides an asynchronous communication function, called
send
that has pushback. That means asend
will be evaluated when thegen_server
accepts the message but will not wait for thegen_server
to complete the processing of the message. - Has the concept of process linking, however it is not preemptive. When a
gen_server
stops, for any reason, any calls tosend
will return an error stating thegen_server
has closed itself. This will not force the termination of any othergen_server
s in Ocaml, but the termination can at least be detected. - Any thrown exceptions are handled by the
gen_server
framework and result in thegen_server
being gracefully terminated.
Relative to Erlang the Ocaml version isn't very impressive, however it's still a useful technique for encapsulating state in a concurrent environment.
This implementation of gen_server
is on top of Jane St's Async. What does it look like? The primary interface looks like this:
val start :
'i ->
('i, 's, 'm, 'ie, 'he) Server.t ->
('m t, [> 'ie init_ret ]) Deferred.Result.t
val stop :
'm t ->
(unit, [> `Closed ]) Deferred.Result.t
val send :
'm t ->
'm ->
('m, [> send_ret ]) Deferred.Result.t
The interface is only three functions: start
, stop
and send
.
- The
start
function is a bit harry looking but don't be put off by the server type parameterized on five type variables. Thestart
function takes two parameters, the first is the initial parameters to pass to thegen_server
, the second is the callbacks of thegen_server
. stop
takes agen_server
and returnsOk ()
on success andError `Closed
if thegen_server
is not running.send
takes agen_server
and a message. The message must be the same type thegen_server
accepts. It returnsOk msg
on success andError `Closed
if thegen_server
is not running.
The most confusion part is probably the ('i, 's, 'm, 'ie, 'he) Server.t
. This is the type that the implementer of the gen_server
writes. It is three callbacks: init
, handle_call
and terminate
. Let's breakdown the type variables:
- 'i - This is the type of the variable that you pass to
start
and will be given to theinit
callback. - 's - This is the type of the state that the
gen_server
will encapsulate. This will be passed tohandle_call
andterminate
. Thehandle_call
callback will manipulate the state and return a new one. - 'm - This is the message type that the
gen_server
will accept. - 'ie - This is the type of error that the
init
callback can return. - 'he - This is the type of error that the
handle_call
callback can return.
While the server type looks complicated, as you can see each variable corresponds to all of the type information needed to understand a gen_server
. So what does a server look like? While the types are big it's actually not too bad. Below is an example of a call to start
. The full source code can be found here.
(* Package the callbacks *)
let callbacks =
{ Gen_server.Server.init; handle_call; terminate }
let start () =
Gen_server.start () callbacks
And what do the callbacks look like? Below is a simplified version of what a set of callbacks could look like, with comments.
module Resp = Gen_server.Response
module Gs = Gen_server
(* Callbacks *)
let init self init =
Deferred.return (Ok ())
let handle_call self state = function
| Msg.Msg1 ->
(* Success *)
Deferred.return (Resp.Ok state)
| Msg.Msg2 ->
(* Error *)
Deferred.return (Resp.Error (reason, state))
| Msg.Msg3 ->
(* Exceptions can be thrown too *)
failwith "blowin' up"
(* Exceptions thrown from terminate are silently ignored *)
let terminate reason state =
match reason with
| Gs.Server.Normal ->
(* Things exited normally *)
Deferred.unit
| Gs.Server.Exn exn ->
(* An exception was thrown *)
Deferred.unit
| Gs.Server.Error err ->
(* User returned an error *)
Deferred.unit
There isn't much more to it than that.
A functor implementation is also provided. I prefer the non-functor version, I think it's a bit less verbose and easier to work with, but some people like them.
How To Get It?
You can install gen_server
through opam
, simply: opam install gen_server
The source can be found here. Only the tags should be trusted as working.
There are a few examples here.
Enjoy.
No comments:
Post a Comment