Friday, May 9, 2008

"Hello, World" Revisited - Automatic Reloading

My last post contained some code for a minimal "Hello, World" webapp in Erlang. However, that code wasn't very Erlangy - if you wanted to make a change to the application, you had to kill the shell and restart it. That's clearly not going to impress the "nine nines uptime" crowd. Plus it's annoying to develop that way.

So here's some bonus code that will cause the server to compile and reload changes on the fly, without having to restart the server. It probably won't get you to nine nines, but it's a start. Just append the following to the original "hello_world.erl" file:


reload(SessionID, Env, Input) ->
case make:all([load]) of
up_to_date ->
hello_world:service(SessionID, Env, Input);
_ ->
mod_esi:deliver(SessionID, [
"Content-Type: text/plain\r\n\r\n",
"compilation error"
])
end.

The reload function acts as a wrapper around our original service function, calling make:all (which will compile and reload any out of date code) before forwarding the request.

You'll also need to stick an export at the top of the file, after the module declaration:

-export([reload/3]).

Last but not least, you'll need to create a file called "Emakefile", which tells Erlang what to compile during the call to make:all. In our case, the file is pretty simple:

{'*', []}.

Now start up the erl shell and run the same steps as before:

Eshell V5.6 (abort with ^G)
1> c(hello_world).
{ok,hello_world}
2> inets:start().
ok
3> hello_world:start().
{ok,<0.51.0>}

But this time, use the following URL to test the app, which will hit our new reload function:

http://localhost:8081/erl/hello_world:reload

You should now be able to make changes to hello_world.erl (e.g. change the message from "Hello, world" to something else), hit refresh in your browser, and immediately see the changes.

What's really happening here?

When I first started learning Erlang, I'd assumed that whenever you reloaded modules (via make:all or code:load_file or whatever) that those changes would go into effect immediately, similar to the way the load command works in Ruby. However, that's not quite how things work - just because changes to a module have been loaded doesn't necessarily mean they will be executed. This is because Erlang gives you very fine grained control over when code changes take effect.

The key to all this is the ':' operator. On the surface, it just looks like a namespace separator, used to disambiguate between functions with the same name in different modules. For example, within our hello_world module, you would think that the following two lines of code would be identical, and that the hello_world: in the first line would be redundant:

hello_world:service(SessionID,Env,Input).
service(SessionID,Env,Input).

However, they're not quite the same. The call in the first line actually checks to see if a newer version of the hello_world module has been loaded, and if so, dispatches to that version. The call in the second line always dispatches to the same version as is currently running, even if a newer version has been loaded. If a new version of the module has not been loaded, their behavior is the same.

The upshot of this is that you can have two versions of your code running at the same time (Erlang doesn't let you have more than two, however). This is similar to the way some web servers have a "graceful" restart option, that allows currently executing requests to continue using the old configuration, while new requests use a different configuration. Except in Erlang you can pick the exact function call in your application where these upgrades happen. Very cool.

You can test this behavior by leaving out the hello_world: prefix in the fourth line of the reload function. You'll notice that your changes are no longer picked up immediately, but are picked up on the next invocation (since the initial call to hello_world:reload by the HTTP server will trigger an update).

No comments: