Reloading rails middleware without restarting the server in development
Asked Answered
O

4

13

I have a rails 4 app with middleware located at lib/some/middleware.rb which is currently injected into the stack though an initializer like so:

MyApp::Application.configure.do |config|
    config.middleware.use 'Some::Middleware'
end

Unfortunately, any time I change something I need to restart the server. How can I reload it on each request in development mode? I've seen similar questions about reloading lib code with either autoloading or wrapping code in a to_prepare block but I'm unsure how that could be applied in this scenario.

Thanks, - FJM

Update #1

If I try to delete the middleware and then re-add it in a to_prepare block I get an error "Can't modify frozen array".

Osburn answered 1/1, 2014 at 23:8 Comment(2)
put your middleware in app/middlewares and try again. it's also important to use a string when adding it to the middleware stack and not the class itself as it will not get reloaded otherwise.Septi
@Septi I tried your suggestion but I was not successful.Osburn
S
17

I thought that at some point Rails was smart enough replacing middleware code at runtime, but I may be wrong.

Here is what I came up with, circumventing Ruby class loading craziness and leveraging Rails class reloading.

Add the middleware to the stack:

# config/environments/development.rb
[...]
config.middleware.use "SomeMiddleware", "some_additional_paramter"

Make use of auto-reloading, but make sure that the running rails instance and the already initialized middleware object keep "forgetting" about the actual code that is executed:

# app/middlewares/some_middleware.rb
class SomeMiddleware
  def initialize(*args)
    @args = args
  end

  def call(env)
    "#{self.class}::Logic".constantize.new(*@args).call(env)
  end

  class Logic
    def initialize(app, additional)
      @app        = app
      @additional = additional
    end

    def call(env)
      [magic]
      @app.call(env)
    end
  end
end

Changes in Logic should be picked up by rails auto reloading on each request.

I think that this actually might become a useful gem!

Septi answered 24/1, 2014 at 2:41 Comment(4)
That's amazing. What is it about constantizing that causes it to reload?Osburn
I think rails is properly reloading the code, but since the middleware is loaded during initialization, you will have to make sure that the code will get re-evaluated on each request. if you stick to the real class calling .new it will have problems, because of the reloading, there are now two Logic classes in the object tree. that is not good. so that's why you have to pass the logic to a different class and also don't keep a reference to the class constant.Septi
@FrankJosephMattia i created an alpha version of a drop-in gem that should take care of reloading in development: github.com/phoet/zazickiSepti
Looks good. I ended up wrapping your original suggestion up into a mixin that I include in my middleware classes. I'll have to give this a shot though.Osburn
T
2

Building up on @phoet's answer we can actually wrap any middleware with this kind of lazy loading, which I found even more useful:

class ReloadableMiddleware
  def initialize(app, middleware_module_name, *middleware_args)
    @app = app
    @name = middleware_module_name
    @args = middleware_args
  end

  def call(env)
    # Lazily initialize the middleware item and call it immediately
    @name.constantize.new(@app, *@args).call(env)
  end
end

It can be then hooked into the Rails config with any other middleware as its first argument, given as a string:

Rails.application.config.middleware.use ReloadableMiddleware, 'YourMiddleware'

Alternatively - I packaged it into a gem called reloadable_middleware, which can be used like so:

Rails.application.config.middleware.use ReloadableMiddleware.wrap(YourMiddleware)
Tawanda answered 20/4, 2019 at 18:31 Comment(0)
P
1

In Rails 6 with the new default Zeitwork code loader, this works for me:

# at the top of config/application.rb, after Bundler.require    

# Load the middleware. It will later be hot-reloaded in config.to_prepare
Dir["./app/middleware/*.rb"].each do |middleware|
  load middleware
end

Below it in the section that configures your class Application, add hot-reloading in config.to_prepare:

middleware = "#{Rails.root}/app/middleware"
Rails.autoloaders.main.ignore(middleware)

# Run before every request in development mode, or before the first request in production
config.to_prepare do
  Dir.glob("#{middleware}/*.rb").each do |middleware|
    load middleware
  end
end
Predatory answered 27/6, 2020 at 13:53 Comment(0)
B
0

Can you not simply use shotgun? If I understand your question you want to ensure the environment reloads on every change you make to your code. That is what shotgun will do.

Bikales answered 1/1, 2014 at 23:40 Comment(2)
I looked into shotgun but it appears to reload everything on every request which strikes me as horribly excessive for my needs. I would like to somehow unload and reload my Some::Middleware class the same way my models/controllers are.Osburn
It is horribly excessive conceptually. But in a development context I have found it excellent in actual practise. Since I started using it, it has never noticeably affected performance or introduced any unexpected issues. But yes from a aesthetic point of view it is ugly.Bikales

© 2022 - 2024 — McMap. All rights reserved.