Improved OCaml Memcached client with Core and Async
17 Sep 2014In the previous blog post, we implemented a simple OCaml library for talking to Memcached with the binary protocol using bitstring. The code uses the baked-in standard library and synchronous IO (blocking), so a lot of time will be wasted waiting for IO. The standard library replacement Core offers cooperative threading with callbacks through Async, similar to Javascript, EventMachine for Ruby or many others. In this blog post we’ll try to rewrite the code from the previous post to use asynchronous IO with Async.
IO with Async
The primary IO abstractions in the OCaml standard library are in_channel and out_channel, while in Core/Async it’s Reader and Writer. Reading and writing with Core/Async is asynchronous (non-blocking), so results are not returned immediately. If we for example examine the signature of Reader.really_read
it looks like this:
The type 'a Deferred.t
signifies that the result of type 'a
is not immediate, but will be filled in sometime in the future. To actually access the result, you can bind functions to be called when the Deferred is resolved using Deferred.bind
:
Note that bind
applies a function to the resolved value and must return a new Deferred value. That’s why we’re using return buf
(type string Deferred.t
) instead of just buf
(type string
). The type of return
is 'a -> 'a Deferred.t
.
To make things more readable, there are two handy infix operators for working with Deferreds:
Let’s turn to Writer
. The most basic function it offers is write
:
You might notice that write
does not return a deferred: write
only queues the write in a buffer, and another Async microthread actually writes to the OS buffer. If you want to ensure that the write has been flushed, you can call Writer.flush : unit -> unit Deferred.t
. An exception will be raised if any errors occur while transferring data to the OS buffer. How to handle exceptions in Async is a topic of another blog post (if you’re curious start here).
We’ve covered enough to update the client library to use Async now, but if you want to learn more about Async, you can also read Dummy’s Guide to Async from Jane Street or the excellent chapter on Async in Real World OCaml.
Updating the Client
We can reuse the code from the last post, which doesn’t touch IO. All the functions read_*
or write_*
needs to be rewritten to be asynchronous though. Let’s start by writing a packet:
This is almost identical to the previous version and doesn’t even introduce any deferreds. Note that we have to convert our bitstrings to strings though, while we could previously emit them directly to the channel (e.g. Bitstring.to_chan out_chan header_bits
). The bitstring library was not built with Async in mind.
Reading the response bears less resemblance with the former version and we now have to use deferreds:
Like before, there’s a slight mismatch between Async and bitstring, and we have to convert between string and bitstring.
Finally, we can now write our small sample program again which connects to a local Memcached server and reads the key “foo”:
Recap
So what did we gain by rewriting our IO to use Core/Async rather than the built-in standard library? In short, our Memcached client library will play nice with other Async code and will scale much more nicely, e.g. if being used as part of a web server based on Async. As noted a couple of times during the rewrite, Async and bitstring doesn’t play very well together. We both need to allocate temporary strings and convert between string and bitstring in multiple places. In a future post I’ll discuss other ways of using Reader/Writer which is more efficient.
If you liked this post, please vote on Hacker News.