• Making Rack::Reloader work with Sinatra

    by Ben

    According to the Sinatra FAQ, source reloading was taken out of Sinatra in version 0.9.2 due to "excess complexity" (in my opinion, that's a great idea, because it's not a feature that needs to be in minimal a web framework like Sinatra). Also, according to the FAQ, Rack::Reloader (included in Rack) can be added to a Sinatra application to do source reloading, so I decided to try it out.

    Setting up Rack::Reloader is easy:

    require 'sinatra'
    require 'rack'
    
    configure :development do
      use Rack::Reloader
    end
    
    get "/hello" do
      "hi!"
    end
    
    $ ruby hello.rb
    == Sinatra/0.9.4 has taken the stage on 4567 for development with backup from Thin
    >> Thin web server (v1.2.4 codename Flaming Astroboy)
    >> Maximum connections set to 1024
    >> Listening on 0.0.0.0:4567, CTRL+C to stop
    [on another terminal]
    $ curl http://localhost:4567/hello
    hi!
    

    If you add another route, you can access it without restarting Sinatra:

    get "/goodbye" do
      "bye!"
    end
    
    $ curl http://localhost:4567/goodbye
    bye!
    

    But what happens when you change the contents of a route?

    get "/hello" do
      "greetings!"
    end
    
    $ curl http://localhost:4567/hello
    hi!
    

    You still get the old value! What is going on here?

    Rack::Reloader simply looks at all files that have been required and, if they have changed on disk, re-requires them. So each Sinatra route is re-evaluated when a reload happens.

    However, identical Sinatra routes do NOT override each other. Rather, the first route that is evaluated is used (more precisely, all routes appended to a list and the first matching one is used, so additional identical routes are never run).

    We can see this with a simple example:

    require 'sinatra'
    
    get "/foo" do
     "foo"
    end
    
    get "/foo" do
     "bar"
    end
    
    $ curl http://localhost:4567/foo
    foo   # The result is 'foo', not 'bar'
    

    Clearly, Rack::Reloader is not very useful if you can't change the contents of any route. The solution is to throw away the old routes when the file is reloaded using Sinatra::Application.reset!, like so:

    configure :development do
      Sinatra::Application.reset!
      use Rack::Reloader
    end
    
    $ curl http://localhost:4567/hello
    greetings!
    

    Success!

    A word of caution: you MUST call reset! very early in your file - before you add any middleware, do any other configuration, or add any routes.

    This method has worked well enough for our Sinatra application. However, code reloading is always tricky and is bound to occasionally produce some weird results. If you want to significantly reduce the chances for strange bugs (at the expense of code loading time), try Shotgun or Rerun. Happy reloading!

    Devver Caliper: Hosted metric_fu for your Ruby project.
    Get set up in under a minute

    Posted on December 21st, 2009 by Ben in Hacking, Tips & Tricks, Tools and tagged , , .