Project

General

Profile

Actions

Bug #20314

open

Simultaneous Timeout expires may raise an exception after the block

Added by mame (Yusuke Endoh) 2 months ago. Updated 2 months ago.

Status:
Open
Assignee:
-
Target version:
-
[ruby-core:117003]

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.

Actions

Also available in: Atom PDF

Like0
Like0Like0Like0