Resolve Rails Reloader and ActiveRecord middleware incompatibility#3166
Conversation
The ActiveRecord middleware releases any current ActiveRecord connections before the reloader has a chance to clear their query caches. We can just remove this middleware because the reloader takes care of clearing active connections, too.
|
The test failure looks like an unrelated jruby concurrency issue? |
| # We need to make sure folks remove this middleware if they've added | ||
| # it themselves. | ||
| # | ||
| if defined?(Sidekiq::Rails::Reloader) && Sidekiq.options[:reloader].is_a?(Sidekiq::Rails::Reloader) |
There was a problem hiding this comment.
Might be simpler to just throw this condition around the clear_active_connections! call... not sure.
There was a problem hiding this comment.
I did that initially but moved it here hoping that it might raise earlier in the Sidekiq boot, but sadly it only raises during a job run. Happy to move it back if it seems more sensible.
| # it themselves. | ||
| # | ||
| if defined?(Sidekiq::Rails::Reloader) && Sidekiq.options[:reloader].is_a?(Sidekiq::Rails::Reloader) | ||
| raise ArgumentError, "Your are usign the Sidekiq ActiveRecord middleware and the new Rails 5 reloader which are incompatible. Please remove the ActiveRecord middleware from your Sidekiq middleware configuration." |
| # cache data. | ||
| # | ||
| # We need to make sure folks remove this middleware if they've added | ||
| # it themselves. |
There was a problem hiding this comment.
Probably worth an explicit acknowledgement that this middleware is unnecessary because the reloader will do the job for us -- that it's not "incompatible so you're trading one feature for the other", it's "incompatible because duplication"
There was a problem hiding this comment.
(I'd put it in the commit message but not in the comment!)
| m.add Middleware::Server::Logging | ||
| m.add Middleware::Server::RetryJobs | ||
| if defined?(::ActiveRecord::Base) | ||
| if defined?(::ActiveRecord::Base) && (!defined?(Sidekiq::Rails::Reloader) || Sidekiq.options[:reloader].is_a?(Sidekiq::Rails::Reloader)) |
There was a problem hiding this comment.
Yep, despite my testing that slipped through!
There was a problem hiding this comment.
I've combined the conditional to match the one in the middleware and negated the whole thing.
|
I think at boot time the code should either add the middleware to the default server-side list or activate the Reloader. Mutually exclusive. |
|
@mperham that's what we're doing? We're also raising in the middleware in case somebody has explicitly added the ActiveRecord middleware themselves outside of the default middleware stack. Although I've just realised that initialize has already happened by the time the reloader gets set, so if the user customises their middleware stack which lazily resolves the default server midddleware then it will have included the ActiveRecord middleware anyway. There's a chicken and egg. We need to change initialization based on the presence of the reloader, but the reloader can't be present until we've initialized. I'll investigate. |
|
Yep, all this logic should go in sidekiq/rails.rb or cli.rb.
|
|
How about this version using railtie hooks within We can't add the middleware earlier because we need at least The reloader needs to come quite late in case cache_classes is changed. We could probably pull the reloader into the same block as the middleware except cache_classes might change later which would miss the code reloading log entry. This setup should allow users to change middleware in |
|
Looks pretty good to me now. Is the runtime check in the middleware still necessary? That's constant overhead that the user will have to pay for every job executed. |
Also clarify exactly when these hooks are run.
|
I've changed the middleware hook to even earlier which hopefully might even cover cases where sidekiq configuration is performed in config/application.rb. The runtime check in the middleware is a just-in-case for folks who might have explicitly added the ActiveRecord middleware themselves. If you think this is a remote enough chance then maybe we could drop it? Oh, or we could move the check to when we setup the reloader and explode there if we detect the ActiveRecord middleware in the middleware chain. I'll draft that up. |
| # The reloader also takes care of ActiveRecord but is incompatible with | ||
| # the ActiveRecord middleware so make sure it's not in the chain already. | ||
| if defined?(Sidekiq::Middleware::Server::ActiveRecord) && Sidekiq.server_middleware.exists?(Sidekiq::Middleware::Server::ActiveRecord) | ||
| raise ArgumentError, "You are using the Sidekiq ActiveRecord middleware and the new Rails 5 reloader which are incompatible. Please remove the ActiveRecord middleware from your Sidekiq middleware configuration." |
There was a problem hiding this comment.
I'm not sure if this should raise, Sidekiq.logger.warn, Rails.logger.warn, or ActiveSupport::Deprecation.warn. Deprecation feels the most rails-ey. Thoughts?
This executor currently relies on `ActiveRecord::Base.connection` not
changing between `prepare` and `complete`. If something else returns
the current ActiveRecord connection to the pool early then this
`complete` call will fail to clear the correct query cache and restore
the original `query_cache_enabled` status.
This has for example been happening in Sidekiq:
sidekiq/sidekiq#3166
We can just keep track of the connection as part of the exector state.
This executor currently relies on `ActiveRecord::Base.connection` not
changing between `prepare` and `complete`. If something else returns
the current ActiveRecord connection to the pool early then this
`complete` call will fail to clear the correct query cache and restore
the original `query_cache_enabled` status.
This has for example been happening in Sidekiq:
sidekiq/sidekiq#3166
We can just keep track of the connection as part of the exector state.
This executor currently relies on `ActiveRecord::Base.connection` not
changing between `prepare` and `complete`. If something else returns
the current ActiveRecord connection to the pool early then this
`complete` call will fail to clear the correct query cache and restore
the original `query_cache_enabled` status.
This has for example been happening in Sidekiq:
sidekiq/sidekiq#3166
We can just keep track of the connection as part of the exector state.
The new Rails reloader enables the query cache within every yield and expects to have access to the current connection that it enabled the query cache for at the end of the block to clear the cache and then release the connection. The Sidekiq ActiveRecord middleware which is added automatically releases the connection earlier, the query cache of another connection may instead be cleared, and the dirty query cache might be used by another worker.
This change makes sure that the ActiveRecord middleware cannot be used in tandem with the new Rails reloader (in case folks have added it to their middleware stacks explicitly) and removes it from the default middleware stack when the reloader is in play.
I couldn't figure out a way to raise the error from the middleware during startup. It seems the middleware is instantiated per job and so the raise only happens when a job is run? Not sure if there's a better way.
Fixes the query cache issue in #3157.
/cc @mperham @matthewd