madlep.com Julian Doherty

Rack::Reloader not reloading? There's `to_app` for that

I burnt a bunch of time over the last few days trying to get the Rack::Reloader middleware to… you know… reload stuff.

TL;DR - call to_app on your Rack::Builder block

I had a Rack app that was being mounted in a parent Rails app. Not wanting to stop/start Rails whenever I tweaked that little Rack app, I figured I’d try to get code in the app auto loading after each request. Tried (and failed) to do it as part of the parent Rails app, so I added use Rack::Reloader in to the middleware stack.

Because this is a mapped rack app, it didn’t have it’s own config.ru to set up the middleware. Instead, the Rack app that gets mounted in Rails routes.rb file is an instance of Rack::Builder. Something like this:

class HelloRack
  def self.call(env)
    [200, {}, ["Hello, World"]]
  end
end
require 'hello_rack'

# WARNING! This doesn't reload... don't copy+paste this as the solution. Read on for the answer!
MyApp = Rack::Builder.new do
  use Rack::Reloader
  run HelloRack
end

And then in our Rails routes file, we hook it all up

Rails.application.routes.draw do
  ...
  mount MyApp, at: "/my_app".
  ...
end

This all looks good, and when we change our code in hello_rack.rb it should pick it up, right?…

So I try it out. Then… nothing. No reloading… No reason why.

Time to crack open the Rack source code…

debugging

Later that day…

It turns out that using the Rack::Builder instance directly as the Rack app causes it to recreate the middleware stack every time a request is made - which calls .new on each middleware class defined.

Normally this is fine, but Rack::Reloader keeps state of what source files are loaded, and the last modified time of them. If the Rack::Reloader instance doesn’t know about the file, it records it’s current modified time, and moves on without reloading it - the assumption being that if the app has just started, then the file doesn’t need to be reloaded - because it’s only just been loaded.

Except it hasn’t just been loaded - Rack::Reloader only thinks it has, as it’s a brand new instance, and this is the first time it’s ever seen the file. By creating a new Rack::Reloader instance for every request, all the state about file modified time gets thrown away, and nothing ever gets reloaded.

The fix? #to_app

The fix for this is really easy. Just call Rack::Builder#to_app and use that instead of using the Rack::Builder instance directly.

#to_app generates the Rack app with all the mappings and middleware in place - which is what we want - but only once. Rack::Builder#call delegates to that every time causing brand new middelware to be instantiated for every request (I’m not sure if that is a bug or a feature…)

If we change our my_app.rb file to look like this: everything works beautifully.

require 'hello_rack'

MyApp = Rack::Builder.new do
  use Rack::Reloader
  run HelloRack
end.to_app # note `to_app` here - that's the secret sauce

Note that you won’t see this problem if you’re just writing a rackup file (i.e. config.ru) and using rackup to execute that - rackup calls Rack::Server.start - which does exactly this under the hood.

Programming - minutes of addictive joy from the flow of writing code, then hours of staring at the screen swearing trying to untangle which library is making everything behave weirdly.

comments powered by Disqus