Sync’ up! … without getting drained

feb 7

Hot routes with cowboy

When evolving a never-should-stop OTP application that talks to the world via the web, there’s a corner case with the popular cowboy web server that needs some explanation.

Cowboy routes are mappings from request URLs to handler code, and this configuration is typically located in the parent application module for that OTP application.

Not all systems, especially web applications, need to be upgraded without being taken out of service. But at Syncpup, we need it. We do ‘relups’ all the time: we don’t take our service down whenever updates are pushed live.

With cowboy, loading new route-mappings is built right into the library. But, there’s a nice pattern we use at Syncpup so that this is never a manual process.

Not entirely obvious to some, this is how we accomplish updating new routes during a real-time release upgrade.

Getting familiar

First, let’s take a look at the procedure for updating the cowboy routes when done manually.

Taking for granted what the routes actually are, we will just gloss over that term and say it lives in the routes/0 routine for the moment. We will pretend this routine lives in — and is exported from — a module named ‘lava_web_app.erl.’ We can access it via the Erlang shell as follows:

1> A = lava_web_app:routes().
2> Name = main_http_listener. % or, what have you
3> cowboy:set_env(Name, dispatch, cowboy_router:compile(A)).

There, in expression three, we have performed the update to the cowboy router & have updated the URL/handler mapping in one go.

Now, about that ‘routes’ routine: just to flesh it out, the following is an example of what it could be. If you’re familiar with cowboy, you’ll recognize it:

...
routes() ->
    [{'_', [
      {<<"/">>, cowboy_static,
      {priv_file, lava_web, "index.html"}},
      {<<"/auth/twitter/callback">>, 
      lava_web_html_handler, [
        {type, twitter}]},
      {<<"/login">>, lava_web_html_handler, [
        {type, login}]},
      {<<"/logout">>, lava_web_html_handler, [
        {type, logout}]}]}].
...

You’ll notice that we are serving four routes: one static, and three handled by a made-up handler lava_web_html_handler.

Relup & snappy routing updates

If we were to add a mapping to our routes, in order to get our cowboy web server to acknowledge the new resource, we’d have to run those three Erlang expressions earlier after tweaking the ‘routes’ routine. In a hot-code release update, it’s incredibly important to automate this. It’s simply not going to cut it to attach to a shell and do this by hand.

The way to handle this is via our application’s ‘appup’ file. So as we migrate to a new version, we explicitly want to:

  1. load the new ‘lava_web_app.erl’ file, which includes the updated routes/0 routine
  2. instruct cowboy to adjust the routes

There’s no better place than ‘lava_web_app.erl’ to house our routing update code, so let’s write & export a routine called update_routes/0.

That code looks as follows:

...
update_routes() ->
    A = routes(),
    Name = main_http_listener,
    cowboy:set_env(Name, dispatch, cowboy_router:compile(A)).
...

With this exported routine, we’re now able to connect all the dots for a ‘relup.’

We’re going to keep things simple and assume the only new code in our new release is that of a new route. Everything else will be the same, even though in a normal circumstance, we would expect the handler to be sporting new code as well. But let’s just take that for granted. So with that assumption made, here’s the updated routes/0 routine, as well as ‘lava_web.appup.src’ which contains the instructions on how to deal with migrating to & from the old version.

‘src/lava_web_app.erl’ :

...
routes() ->
    [{'_', [
      {<<"/">>, cowboy_static,
      {priv_file, lava_web, "index.html"}},
      {<<"/auth/twitter/callback">>, 
      lava_web_html_handler, [
        {type, twitter}]},
      {<<"/auth/facebook/callback">>, % N.B.
      lava_web_html_handler, [
        {type, facebook}]},
      {<<"/login">>, lava_web_html_handler, [
        {type, login}]},
      {<<"/logout">>, lava_web_html_handler, [
        {type, logout}]}]}].
...

‘src/lava_web.appup.src’ :

{"1986.b",
  [{"1986.a", [
    {load_module, lava_web_app},
    {apply, {lava_web_app, update_routes, []}}]}],
  [{"1986.a", [
    {load_module, lava_web_app},
    {apply, {lava_web_app, update_routes, []}}]}]
}.

And with that, we are all set to proceed with a real-time upgrade with our new target release.

Minding Ps & Qs

As with any target release upgrade, there are quite a few things that can go wrong. Therefore, if you have a system that can go off-line for a moment during a system upgrade, then do yourself a favor: bypass ‘relups’ altogether.

For those who must maintain uptime during upgrades, there’s really nothing to fear if you stay cool headed. Just be sure to try things out in mock runs, make a checklist that’s easy to follow & deploy with confidence. There’s no better feeling than a ‘relup’ gone right!