Don't apply locking around basic #load / #require#20989
Conversation
That's outside our remit, and dangerous... if a caller has their own locking to protect against the natural race danger, we'll deadlock against it.
Don't apply locking around basic #load / #require
|
See also #20928 (comment). @thedarkone's conclusion there holds for anything that's triggering "actual" constant autoloading inside some 3rd party lock... this PR reflects the fact that this particular instance was instead caused more by my overreach when adding locking in #17102. |
|
@matthewd you brought this on yourself Now with d517239 I see a problem: we're accessing |
|
@thedarkone ah, good catch! 😕 I think we can add a: def exclusive?
@exclusive_thread == Thread.current
end(because Or do you think we should just switch the watch stack to a TS::Array? Actually... given that it's only mutated when the load lock is held... are we already safe? I'll defer entirely to your expertise on thread safety of ruby primitives... but if the only thing we're doing concurrently is calling |
|
No no no, I sidestepped the thread-safety
True, What I mean is, for example, a normal Rails request dispatch thread is in an I think we can fix this with your suggested if Dependencies.load? && ActiveSupport::Dependencies.constant_watch_stack.watching? && Dependencies.interlock.exclusive?
Dependencies.new_constants_in(Object) { yield }
else
yield
end |
|
Ah, right.. because this is up in On the other hand, I guess that's technically true even today: any code running outside the protection of Rack::Lock is in danger of interfering... Hmm... maybe that's by design? Or we technically need another separate lock? 😱 If we just changed that conditional to ensure we hold the exclusive lock, the result would be whatever constants we load, getting misattributed to whichever file was being simultaneously autoloaded. (Note, the only purpose of this one is to ensure they don't show up in the other one.) So, I think it's correct that we go down the first branch there, even in other threads... but it does seem separately race-prone. 😕 |
|
Progress! 🎉 No more lockups but I can't get the code to reload. I fire off a bunch of jobs, I see the expected time output. I edit hard_worker.rb to change the time output and fire off another set of jobs. The time output remains as the original. Should |
That is technically true, but on the other hand if anyone opens an issue reporting that const loading is broken when multiple threads are used, it is swiftly closed with an explanation that const loading is single threaded only. You're right that an additional @mperham as per original discussion #17102 (comment), it is expected that |
|
How do I recreate the code reloading middleware without needing to build an entire fake Rack stack? |
|
@mperham for now, I think you'll need to do something equivalent to rails/railties/lib/rails/application/finisher.rb Lines 88 to 111 in 8f81f7a rails/railties/lib/rails/application/finisher.rb Lines 89 to 92 in 8f81f7a There's obviously still some API work to be done, to make this process nicer, and avoid you duplicating details you shouldn't need to know about. |
|
It's alive and working! This is great, thank you so much for your help. Does this look reasonable and optimal to you, to reload the code after every job has executed? |
|
@mperham that would work, but it is going to attempt to unload/reload the whole app on each job, this also ignores changes to Note that my code won't work under concurrent load (yet) unless #21006 is fixed. |
|
Thank you so much, I have an updated version working in Sidekiq now. routes don't matter to Sidekiq but most everything else does. I think the 99% use case is little to no load in development mode. I can't think of a time where I had Sidekiq processing something while editing code. It's definitely a price I can bear. |
That's outside our remit, and dangerous... if a caller has their own locking to protect against the natural race danger, we'll deadlock against it.
@mperham reported an issue whereby one thread would end up waiting at:
with others stuck at:
So, thread A acquires the connection_pool lock, and is able to proceed. Meanwhile thread B enters app space. A now attempts to make an explicit
requirecall (which would be naturally safe; protected by ruby's internalloadingtracking), but we intervene and force it to first acquire our load lock -- it must now wait until thread B either completes, or hits a load-lock acquisition of its own. B now follows the same path, and blocks on its attempt to acquire the connection_pool lock = deadlock.Change the above scenario from an explicit
requireto a mention of a missing constant, and we'll still deadlock after this PR -- but that one is more defensible: we have no other choice. It's also (IMO) a lower risk exposure: "user" code, which is where auto-loadable constants are likely to be mentioned, is unlikely to be casually executing inside an exclusive lock...requireinside an exclusive lock is a not-unreasonable prospect for library code... it's also fixable, by just mentioning the constant before you acquire the other lock. That doesn't apply to
require, because we were acquiring the load lock before we even checked whether the file had already been loaded.Moreover, the previous implementation was Philosophically Wrong: AS::Dep isn't supposed to be enhancing or in any way interfering with the operation of
load/require. It only defines them in order to exclude them from its normal behaviour.To do so would also give a false sense of protection, against a set of dangers that would persist/return when the load interlock is removed (e.g., in a production environment where
eager_loadandcache_classesare both true).