Key/Value Server Part Three

Refactoring the key/value server so that it conforms to Eralng/OTP conventions.

In part one we created an in-memory key/value storage system. Then, in part two we made it persist the state to disk during shutdowns. Now, in this final section, we'll update the server to use the OTP behaviour called gen_server.

Complete code for this tutorial is available in the GitHub repository.

It's Hip To Be Square!

So far in this tutorial series we made a lightweight key/value server that held its state in memory. We then modified the server to store and reload the state so that values would not be lost when the server was shut down. This all works very well, and you'd be justified in relying on this feature in a production system. However, when programming it's good to be a conformist. Following expected conventions and best practices makes maintenance and future changes much easier for you as well as any other programmer who might end up dealing with your software.

It turns out that making small, single-purpose servers (like this one) is an extremely common task in Erlang, and so there already exist excellent facilities to assist you. So, to conform to conventions (and in the spirit of the principle of least astonishment) let's convert our server into an OTP-compliant module by refactoring it to implement the gen_server behaviour. As you'll see, this is really easy, and actually reduces the number of lines of code slightly!

If you are following along, the existing code for the persistent key value server is in the GitHub repository in a file called pkv.erl. The final version, which conforms to the usual OTP conventions is in a file called pkv2.erl.

Refactoring

Let's begin by removing the rpc function (it's not needed any more, since gen_server handles that) and declaring the behaviour:

Next, we'll convert the client API to use the provided message passing functions in the gen_server library:

Notice that the start/0 function has become start_link/0 to conform to convention. This is the only change required to the client-facing API.

To refactor the server implementation, we move the disk persistence features into init/1 and terminate/2:

and add the following three unused callbacks, which are required by the gen_server behaviour:

Finally, we convert the old server loop receive block:

into the equivalent set of synchronous callback handlers:

Something Is Missing

You may have noticed that the server implementation above is missing a stop function (which we added in the previous tutorial). Why is this? When using the gen_server behaviour you have some options available, depending on whether or not you plan to use your module in a supervision tree.

In this case, I do intend to use this module in a supervision tree, so there is no need to add a client function and server handler for stopping the service. The supervision tree will handle that, ensuring that terminate(shutdown, State) (see above) is called.

To use the module on its own, outside of a supervision tree, I would need to add some code to stop the service. The client API function might look like:

stop() -> gen_server:cast(?MODULE, stop).    

And the server code might look like:

handle_cast(stop, State) -> {stop, normal, State}.

terminate(normal, State) ->
    write_to_disk(Dict),
    ok.

You can find complete details about this in the Erlang docs.

Finished!

That's it! The utility functions remain unchanged, so there is nothing else required to make this module conform to Erlang/OTP conventions. The module can be still be used by directly invoking the client API, but it can also now be packaged as a stand-alone OTP application, or included in an existing application's supervision tree.