Bug #15507
closedThread#raise is delivered to GC context
Description
Since I started the development of Eventbox I noticed sporadic failures of the Eventbox test suite on MRI. One issue has been fixed in the meantime, but I was now able to minimize 3 others to a reproducible script. This is the first one:
This script combines a GC finalizer with Thread#raise:
Thread.abort_on_exception = true
class Finalizer
def self.define
ObjectSpace.define_finalizer(String.new, self.method(:gc))
end
def self.gc(object_id)
puts "gc #{object_id} #{caller[0]}"
end
end
class Deadlocker
class Stop < RuntimeError
end
def self.run
th = Thread.handle_interrupt(Exception => :never) do
Thread.new do
begin
Thread.handle_interrupt(Stop => :on_blocking) do
100.times { String.new } # Trigger GC
sleep 5 # Wait for Stop exception
raise "not interrupted"
end
rescue Stop
end
end
end
th.raise Stop
th.join
end
end
100.times do
Finalizer.define # This alone works well
Deadlocker.run # This alone works equally, but both interfere badly
end
The program output looks similar to:
$ ruby -d --disable-gems interrupt-in-gc-error.rb
Exception `Deadlocker::Stop' at interrupt-in-gc-error.rb:23 - Deadlocker::Stop
Exception `Deadlocker::Stop' at interrupt-in-gc-error.rb:23 - Deadlocker::Stop
[...]
Exception `Deadlocker::Stop' at interrupt-in-gc-error.rb:9 - Deadlocker::Stop
gc 47133824012760 interrupt-in-gc-error.rb:22:in `call'
gc 47133824022800 interrupt-in-gc-error.rb:22:in `call'
[...]
Exception `RuntimeError' at interrupt-in-gc-error.rb:24 - not interrupted
#<Thread:0x000055bc65ac8f38@interrupt-in-gc-error.rb:19 run> terminated with exception (report_on_exception is true):
Traceback (most recent call last):
2: from interrupt-in-gc-error.rb:21:in `block (2 levels) in run'
1: from interrupt-in-gc-error.rb:21:in `handle_interrupt'
interrupt-in-gc-error.rb:24:in `block (3 levels) in run': not interrupted (RuntimeError)
The debug output shows, that the Stop
exception is delivered several times, so that the sleep call is properly interrupted. But if the interrupt is sent just in the moment when the finalizer is active, it is discarded and doesn't abort the sleep
call.
IMHO interrupts shouldn't be delivered to any GC/trap context. At least they should be masked in this context.
This issue is present on all older MRI versions. However it doesn't appear on JRuby-9.2.5.0. So they seem to have solved the GC/interrupt relationship somehow.
Updated by larskanis (Lars Kanis) almost 6 years ago
Updated by jeremyevans0 (Jeremy Evans) over 3 years ago
- Is duplicate of Bug #13876: Tempfile's finalizer can be interrupted by a Timeout exception which can cause the process to hang added
Updated by jeremyevans0 (Jeremy Evans) over 3 years ago
I've confirmed this issue would be fixed by https://github.com/ruby/ruby/pull/4366.
Updated by jeremyevans (Jeremy Evans) over 3 years ago
- Status changed from Open to Closed
Applied in changeset git|87b327efe6c1f456c43b3f105c7a92a5a7effe93.
Do not check pending interrupts when running finalizers
This fixes cases where exceptions raised using Thread#raise are
swallowed by finalizers and not delivered to the running thread.
This could cause issues with finalizers that rely on pending interrupts,
but that case is expected to be rarer.
Fixes [Bug #13876]
Fixes [Bug #15507]
Co-authored-by: Koichi Sasada ko1@atdot.net
Updated by kjtsanaktsidis (KJ Tsanaktsidis) almost 2 years ago
❤️ Thank you for fixing this @jeremyevans0 (Jeremy Evans) - I was debugging some instances of our Ruby 2.7 app's test suite hanging on exit, and narrowed it down to this reproduction:
require 'tempfile'
def forget_about_a_tempfile
Tempfile.new('garbage')
end
def tempfile_making_thread
loop do
forget_about_a_tempfile
GC.start
end
end
10.times do
Thread.new { tempfile_making_thread }
end
sleep 0.5
I git bisect'd the fix to this commit. I guess the eTerminateSignal
that kills threads on process exit has the same problem as an explicit Thread#raise
.