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
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
Rack::Reloader in to the middleware stack.
Because this is a mapped rack app, it didn’t have it’s own
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:
1 2 3 4 5
1 2 3 4 5 6 7
And then in our Rails routes file, we hook it all up
1 2 3 4 5
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…
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 –
.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 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.
1 2 3 4 5 6
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 –
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.