12

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".

  • 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. – phoet Jan 22 '14 at 02:20
  • @phoet I tried your suggestion but I was not successful. – Frank Joseph Mattia Jan 24 '14 at 02:11

4 Answers4

15

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!

phoet
  • 18,688
  • 4
  • 46
  • 74
  • 1
    That's amazing. What is it about constantizing that causes it to reload? – Frank Joseph Mattia Jan 25 '14 at 00:46
  • 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. – phoet Jan 25 '14 at 01:12
  • 1
    @FrankJosephMattia i created an alpha version of a drop-in gem that should take care of reloading in development: https://github.com/phoet/zazicki – phoet Feb 11 '14 at 21:06
  • 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. – Frank Joseph Mattia Feb 12 '14 at 02:04
1

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)
Julik
  • 7,676
  • 2
  • 34
  • 48
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
MarkWPiper
  • 863
  • 8
  • 6
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.

Daniel C
  • 1,332
  • 9
  • 15
  • 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. – Frank Joseph Mattia Jan 01 '14 at 23:49
  • 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. – Daniel C Jan 02 '14 at 00:18