Hot Code Swapping

Nick McDowall edited this page May 3, 2017 · 8 revisions

Hot Code Swapping

Wouldn't it be great to be able to upgrade your code or fix bugs without having to bring the whole server or system down? Well Erlang makes this possible by supporting hot code swapping - which is just one of the ways that the language lets you build highly available systems with minimal down-time.

So how does it work? Well it is pretty straight forward and I will attempt to demonstrate how it works by using a simplistic example:

Pricing Service

-module(pricing_service).
-export([link_with/1, message_receiver/0]).

link_with(ClientPid) ->
	register(client, ClientPid),
	spawn_link(pricing_service, message_receiver, []).

message_receiver() ->
	receive
		{price, Item} 	-> reply_with_price_of(Item),
                                   message_receiver()
	end.

reply_with_price_of(Item) ->
	client ! io:format("The price of ~p is: $~p ~n", [Item, price(Item)]).

price(Item) -> 
	case Item of
		tea      -> 2.05;
		coffee   -> 2.10;
		milk     -> 0.99;
		bread    -> 0.50
	end.

The code I have created above is a simple pricing service which accepts requests for prices and returns the corresponding price to the registered client. The first line declares the module name (which will match the file name) and the second line declares the functions that are publicly visible.

The first of the two exported functions is the link_with function which will need to be called first by the client to get the pricing service process id (PID):

link_with(ClientPid) ->
	register(client, ClientPid),
	spawn_link(pricing_service, message_receiver, []).

This function does two things, firstly it accepts an argument which must be the client's PID. It then registers that PID under the name of client which it will use when sending responses to the client. Secondly it calls a built in function (BIF) spawn_link(Module, Function, Args) which will create a process starting in the message_receiver function with no arguments hence the empty list as the third argument. Finally the PID of the spawned process is returned as the result of the function call - which the client will need to be able to send messages to the service.

I have used spawn_link rather than just spawn to create the process so that I get notified if the process dies (I'll visit linking/monitoring in another article).

Let's take a closer look at the message_receiver function that runs when the process is spawned:

message_receiver() ->
	receive
		{price, Item} 	-> reply_with_price_of(Item),
                                   message_receiver()
	end.

The receive clause will cause the process to go and check its inbox for messages - waiting there until it does receive a message. Upon receiving a message it will use pattern matching to decide how to deal with the message. In this case only messages containing {price, Item} where Item is a variable will be processed. Finally after processing the message a looping call back to the function message_receiver is called so that the process will wait for its next message.

The response is constructed and sent in the reply_with_price_of function:

reply_with_price_of(Item) ->
	client ! io:format("The price of ~p is: $~p ~n", [Item, price(Item)]).

Remember that we register the client's PID under the name client when the link_with function gets called and here we can see how the response is sent back to the client using the registered name. Another BIF is used to format a response string that will be returned to the client - io:format(String, Arguments) and the second argument is the result of the price function call which gives us the price of Item.

Lets crank up the shell to compile the service and demonstrate how to send it a message:

	C:\Dev\Erlang-Examples\src>erl
	 Eshell V5.9  (abort with ^G)
	1> c(pricing_service).
	 {ok,pricing_service}
	2> Service = pricing_service:link_with(self()).
	 <0.37.0>
	3> Service ! {price, milk}.
	 The price of milk is: $0.99
	 {price,milk}

So far so good - the Service variable stores the process id of the pricing service which I will need when sending messages to it. I also provide the shell PID when calling link_with by using the BIF self() so that I get the responses in the shell process. On line 3 I send the message {price, milk} to the service and below that you can see the response from the pricing service: The price of milk is: $0.99

Code Swapping

Now that we have a running process that we can interact with it's about time we started trying out some hot swapping magic. So I update the price of milk to $1.03 and recompile the code:

	4> c(pricing_service).
	 {ok,pricing_service}

then request the price of milk again (the process is still running from earlier and has not been stopped):

	5> Service ! {price, milk}.
	 The price of milk is: $0.99
	 {price,milk}

Mmm, that's odd... I get the same price as before.. not what I was expecting but after a bit of research I found out that I need to specify the module name as a prefix to a function call in order to trigger the latest version of that module otherwise it will use the current version of the code.

I then update the message_receiver function as follows:

-export([link_with/1, message_receiver/0, reply_with_price_of/1]).

...
message_receiver() ->
	receive
		{price, Item} 	-> pricing_service:reply_with_price_of(Item),
                                   pricing_service:message_receiver()
	end.
...

Notice that I now need to export the reply_with_price_of function since I am treating it like a public function by prefixing it with the module name. You might be wondering why I have not simply prefixed the looping call to message_receiver and the reason is that the process will be waiting in the receive clause for a message so if the code gets updated it would not get loaded until after the next message was received and processed and then upon looping it would trigger the update.

Now this time I have to stop the process and repeat the steps from earlier before updating the price again and compiling:

4> c(pricing_service).
 {ok,pricing_service}
5> Service ! {price, milk}.
 The price of milk is: $1.03
 {price,milk}

this time we get the change immediately - happy days :-).. but we can do better - what if I want to handle further message types such as requests to find what version of the service is on, then I would need to add the module name as a prefix to the new function call that handles the new message type and export the function too (even though I don't really want to expose all my internal functions!). Things could soon start getting messy so I add the new functionality and then refactor the code as follows:

-module(pricing_service).
-export([link_with/1, message_receiver/0, handle/1]).
-define(VERSION, '2.00').

link_with(ClientPid) ->
	register(client, ClientPid),
	spawn_link(pricing_service, message_receiver, []).

message_receiver()  ->
	receive
        Request     -> pricing_service:handle(Request),
                       pricing_service:message_receiver()
	end.

handle(Request) -> 
	case Request of 
		version           -> reply_with_version();
		{price, Item}     -> reply_with_price_of(Item)
	end.

reply_with_version()      ->
	client ! io:format("Service Version:[~p]~n",[?VERSION]).

reply_with_price_of(Item) ->
	client ! io:format("The price of ~p is: $~p ~n", [Item, price(Item)]).

price(Item) -> 
	case Item of
		tea      -> 2.05;
		coffee   -> 2.10;
		milk     -> 1.17;
		bread    -> 0.50
	end.

The service now delegates all messages it receives to the handle(Request) function which contains the module prefix to trigger the latest version of the module to be used and we can therefore update this function quite happily without messing around with exports and prefixes.

So the service now supports messages that request the version number and will return the value of the macro ?VERSION which is defined at the top of the file (this is basically just a constant value):

6> Service ! version.
 Service Version:['2.00']
 version

Note: that we could use Erlang's built in macro ?MODULE as the module name prefix - this would save us from having to update the prefixes if you ever change the module name.

Note: that the Erlang VM will only allow two versions of the module to be loaded at any one time - so if you try compiling the module twice while the process is running without the new version being loaded you will find that the process gets killed! I plan to explore this further in the near future including looking at using timeouts to refresh the code automatically to avoid this from happening.

You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.
Press h to open a hovercard with more details.