Bug #20314
openSimultaneous Timeout expires may raise an exception after the block
Description
Launchable reports TestTimeout#test_nested_timeout
as a flaky test, and I reproduced it as follows.
require "timeout"
class A < Exception
end
class B < Exception
end
begin
Timeout.timeout(0.1, A) do
Timeout.timeout(0.1, B) do
nil while true
end
end
rescue A, B
p $! #=> #<A: execution expired>
# Exception B is raised after the above call returns
#=> test.rb:16:in `p': execution expired (B)
p :end # not reach
end
This is because the timer thread performs two consecutive Thread#raise
to the target thread.
I have discussed this with @ko1 (Koichi Sasada) and have come up with three solutions.
Solution 1¶
When multiple nested Timeouts expire simultaneously, raise an exception for the outer-most Timeout and let the inner Timeouts expire without throwing an exception. In the above example, it would only raise A.
The problem with this approach is that if you are rescuing A in the inner block, it may never ends:
Timeout.timeout(0.1, A) do
Timeout.timeout(0.1, B) do
begin
sleep
rescue A
sleep # The exception A is caught. The inner Timeout is already expired, so the code (may) never end.
end
end
end
Note that, if A and B did not occur at the same time, it would raise B. This is a race condition.
Solution 2¶
When multiple nested Timeouts expire simultaneously, raise an exception for the inner-most Timeout and let the outer Timeouts wait until the inner-most Timeout returns. In the above example, it would raise either A or B, not both.
The problem with this approach is that if you are rescuing B in the inner block, it never ends:
Timeout.timeout(0.1, A) do
Timeout.timeout(0.1, B) do
begin
sleep
rescue B
sleep # The outer Timeout waits for the inner timeout, and the inner Timeout never return. So this code never ends.
end
end
end
Solution 3¶
Make thread interrupt queue one length. If the target thread has already been Thread#raise(A)
, the new Thread#raise(B)
blocks until the target thread processes A.
Since there will be no more simultaneous Thread#raise, there will be no more exceptions after the end of the block. The timeout timer thread should be changed in consideration that Thread#raise
may block.