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 mount
ed 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…
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.