That broken rubyspec was written by me. The problem lies with repeatedly autoloading the same .rb file, since this should be impossible, the spec manually deletes the loaded path from $LOADED_FEATURES and then re-declares the autoload, this is currently broken on MRI.
Here's a much smaller repro script:
def with_autoload_file(const_name, file_name = 'foo.rb')
mangled_file_name = file_name.sub(/\.rb\Z/, '____temp____autoload.rb') # avoid accidentally overwriting any files
File.write(mangled_file_name, "sleep 1; module #{const_name}; end")
autoload const_name, File.expand_path(mangled_file_name.sub(/\.rb\Z/, ''))
$LOADED_FEATURES.delete(File.expand_path(mangled_file_name)) if $LOADED_FEATURES.include?(File.expand_path(mangled_file_name))
yield
ensure
File.delete(mangled_file_name)
end
foo_ready = bar_waiting = bar_ready = false
t = Thread.new do
Thread.pass until foo_ready
Foo
bar_waiting = true
Thread.pass until bar_ready
Bar
end
with_autoload_file('Foo') do
foo_ready = true
Foo
end
Thread.pass until bar_waiting
with_autoload_file('Bar') do
bar_ready = true
Bar
end
t.join
Running this results in an "uninitialized constant Bar" exception from the non-main thread.
If the last block is rearranged like this:
with_autoload_file('Bar') do
Bar
bar_ready = true
end
the script deadlocks (main thread deadlocks, while secondary thread t busy spins in Thread.pass until bar_ready).
If the last autoload block uses a different .rb file, everything works fine:
with_autoload_file('Bar', 'bar.rb') do
Bar
bar_ready = true
end
I think I've tracked the issue to an incorrectly locked load_lock's thread_shield: when rb_thread_shield_wait() returns Qfalse the failed thread creates a new thread_shield via rb_thread_shield_new(), however because rb_thread_shield_new() automatically locks the newly created shield and the branch does not return a successful ftptr, the newly installed shield is then never unlocked.
The attached patch seems to fix the issue for me.