Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Class reloading stops after uncaught exception in custom middleware

I've written my own middleware to provide an API endpoint to our application. The middleware loads classes that provide the API methods, and routes the request to the appropriate class/method. The classes are loaded dynamically through String#constantize.

While running in development mode, the classes are automatically reloaded. However, if there is an uncaught exception – which is subsequently handled by the Failsafe middleware – the automatic reloading stops working. constantize is still being called but it seems to return the old class.

It would appear there is something else that unloads classes, and an uncaught exception breaks it. What could this be?

Running Ruby 1.8.7, Rails 2.3.3 and Thin 1.2.2.

like image 555
Mark Wubben Avatar asked Aug 27 '09 09:08

Mark Wubben


1 Answers

I think this effect comes from the way ActionController::Reloader is written. Here's ActionController::Reloader#call from 2.3.3, note the comment:

def call(env)
  Dispatcher.reload_application
  status, headers, body = @app.call(env)
  # We do not want to call 'cleanup_application' in an ensure block
  # because the returned Rack response body may lazily generate its data. This
  # is for example the case if one calls
  #
  #   render :text => lambda { ... code here which refers to application models ... }
  #
  # in an ActionController.
  #
  # Instead, we will want to cleanup the application code after the request is
  # completely finished. So we wrap the body in a BodyWrapper class so that
  # when the Rack handler calls #close during the end of the request, we get to
  # run our cleanup code.
  [status, headers, BodyWrapper.new(body)]
end

Dispatcher.reload_application doesn't remove auto-loaded constants, Dispatcher.cleanup_application does. BodyWrapper#close is written with possible exceptions in mind:

def close
  @body.close if @body.respond_to?(:close)
ensure
  Dispatcher.cleanup_application
end

However this doesn't help, because if @app.call in ActionController::Reloader#call throws an exception, BodyWrapper doesn't get instantiated, and Dispatcher.cleanup_application doesn't get called.

Imagine the following scenario:

  • I make changes in one of my files which affects API call
  • I hit API call and see error, at this point all files including the one with a bug aren't unloaded
  • I make a codefix and hit the same API call to check if it worked
  • call gets routed the same way as before, to old classes/objects/modules. This throws same error and again leaves loaded constants in memory

This doesn't happen when traditional controllers raise errors because those are handled by ActionController::Rescue. Such exceptions do not hit ActionController::Reloader.

Simplest solution would be to put fallback rescue clause into API routing middleware, some variation of this:

def call(env)
  # route API call
resuce Exception
  Dispatcher.cleanup_application
  raise
end

Note that this is my answer to 3 year old question and I followed call stack of 2.3.3. Newer versions of rails may handle things differently.

like image 76
Serge Balyuk Avatar answered Sep 27 '22 18:09

Serge Balyuk