Technical Musings: Erlang Hot Code Swapping - Interfaces

Friday, April 15, 2011

Erlang Hot Code Swapping - Interfaces

I read Raymond Tay's 2007 post Erlang Hot Code Swapping a while back, and realized it was not quite right, or at least not complete. The idea and code behind code swapping works, but the execution of the code in the blog didn't actually demonstrate that fact. He changes the calling functions along with swapping the code; you shouldn't have to do that.

Which brings me to interfaces. EJBs (what he was trying to emulate) are all about interfaces. You specifically have to have separate files for the interface and the implementation. In fact, EJB 1.0 was way too heavy with the interfaces, and it was a pain to work with.

But the general idea of an interface is awesome. USB anyone? Back in the bad old days there was the RS-232 serial interface, which was woefully under-standardized, so there was no guarantee that plugging two RS-232 devices together would work (usually the opposite). You'd spend a lot of time trying different configurations to get things work (Remember 9600-N-8-1?).

Interfaces dictate a list of methods (or functions) that can always be expected to be implemented by a piece of code. This is great for long running code that must work with other systems. The implementation changes over time, but the developer knows if he/she (ok, most likely he, but only statistically) is going to change the interface, they will break compatibility with other systems.

The neat thing is that the interface is already defined in Erlang code that implements callbacks; they are just the exported functions that will make the callbacks. But the Java idea of an Interface is a list of methods that are separate from the implementation that can be share across implementations. Interfaces aren't a language feature of Erlang, but OTP behaviours are Interfaces (among other things): They require a list of exported functions.

What's the best way to implement an interface in Erlang without OTP? The best way I've figured is this: Move the public functions from the private callback functions into separate modules, and then import the public functions. This way two implementations that import the interface module will have to share the same interface.

First, the generic container code:

container.erl:
-module(container).
-export([start/1, rpc/2, swap_code/1]).

-include("callback.hrl").

start(Mod) ->
register(?SERVERNAME, spawn(fun() -> loop(?SERVERNAME, Mod, Mod:init()) end)).

swap_code(Mod) -> rpc(?SERVERNAME, {swap_code, Mod}).

%
% Standard code for abstracting the "RPC-call" layer
%
rpc(Name, Request) ->
    Name ! {self(), Request},
    receive
        {Name, Response} -> Response
    end.

%
% Standard code for looping and waiting for messages from clients
%
loop(Name, Mod, OldState) ->
    receive
        {From, {swap_code, NewCallbackMod}} ->
            From ! {Name, ack},
            loop(Name, NewCallbackMod, OldState);
        {From, Request} ->
            {Response, NewState} = Mod:handle(Request,OldState),
            From ! {Name, Response},
            loop(Name, Mod, NewState)
    end.
The registered server name is stored in an .hrl file included in the container and callback code:

callback.hrl:
-define(SERVERNAME, moneyserver).
callback.erl:
-module(callback).
-export([dollarToYen/1, yenToEuro/1]).
-include("callback.hrl").

-import(container, [rpc/2]).

%% client routines
dollarToYen(Dollars) -> rpc(?SERVERNAME, {convertToYen, Dollars}).
yenToEuro(Yen) -> rpc(?SERVERNAME, {convertToEuro, Yen}).
callback_impl.erl:
-module(callback_impl).
-export([init/0, handle/2]).
-import(container, [rpc/2]).
%% client routines
-import(callback, [dollarToYen/1,yenToEuro/1]).

%% callback routines
init() -> dict:new().

handle({convertToYen, Dollars}, Dict) -> { Dollars * 126, Dict};
handle({convertToEuro, Yen}, Dict) -> {Yen * 0.0077, Dict}.

callback_impl2.erl:
-module(callback_impl2).
-export([init/0, handle/2]).
-import(container, [rpc/2]).
%% client routines
-import(callback, [dollarToYen/1,yenToEuro/1]).

%% callback routines
init() -> dict:new().

handle({convertToYen, Dollars}, Dict) -> { Dollars * 126 * 126, Dict};
handle({convertToEuro, Yen}, Dict) -> {Yen * 0.0077 * 0.0077, Dict}.

How this works:

RUNTIME:

> c(container).
> c(callback).
> c(callback_impl).
> c(callback_impl2).

% our first implementation:
> container:start(callback_impl).
> callback:dollarToYen(1).
126
> callback:yenToEuro(1).
0.0077

> container:swap_code(callback_impl2).
ack
> callback:yenToEuro(1).
5.929e-5
> callback:dollarToYen(1).
15876

Same interface, same exact call, but hot swapped implementations!

No comments: