module Timeout
Requiring this file replaces Ruby's stdlib Timeout.timeout
with an implementation that has proper critical section protection.
Background: The implementation of Ruby's Timeout.timeout
method contains an unprotected critical section that may result in unexpected and/or undesired behavior - depending on the client's usage. In MIQ, this bug contributed to the issue described here:
https://bugzilla.redhat.com/show_bug.cgi?id=1207018
Description: The Timeout.timeout
method starts a “timing” thread that sleeps for the specified period of time. Once the timing thread is started, the parent (timed) thread yields control to the code block - the section of code subject to the timeout.
A timeout occurs when the timing thread wakes up before we return from the yield to the code block. Here, the timing thread raises an exception in the timed thread, terminating the execution of the code block.
When the code block doesn't timeout, we return from the yield. The parent thread then kills the timing thread - supposedly preventing it from waking up and raising a timeout exception. I say supposedly, because this is the heart of the unprotected critical section. From the time the yield returns, until the time the timing thread is killed, the timing thread can still wake up and raise an exception in the timed thread. Since the timed thread is no longer executing the code block subject to timeout, this can result in unpredicted and undesired behavior.
This fix employs a Mutex and state flag to protect this critical section of code, preventing this from happening.
TODO: This code is based on the Ruby 2.0 implementation of timeout. When upgrading to new versions of Ruby, we need to reevaluate this code against the new Ruby implementations.
Public Class Methods
# File lib/VMwareWebService/broker_timeout.rb, line 37 def timeout(sec, klass = nil) #:yield: +sec+ return yield(sec) if sec.nil? or sec.zero? exception = klass || Class.new(Timeout::Error) state_lock = Mutex.new state = :sleeping begin begin x = Thread.current y = Thread.start { begin sleep sec rescue => e state_lock.synchronize do if state == :sleeping state = :exception x.raise e end end else state_lock.synchronize do if state == :sleeping state = :timeout x.raise exception, "execution expired" end end end } rv = yield(sec) state_lock.synchronize do if state == :sleeping state = :returned_from_yield return rv end end ensure state_lock.synchronize do if state == :sleeping || state == :returned_from_yield state = :killing if y y.kill y.join # make sure y is dead. end end end end rescue exception => e rej = /\A#{Regexp.quote(__FILE__)}:#{__LINE__ - 4}\z/o (bt = e.backtrace).reject! { |m| rej =~ m } level = -caller(CALLER_OFFSET).size while THIS_FILE =~ bt[level] bt.delete_at(level) level += 1 end raise if klass # if exception class is specified, it # would be expected outside. raise Error, e.message, e.backtrace end end
Private Instance Methods
# File lib/VMwareWebService/broker_timeout.rb, line 37 def timeout(sec, klass = nil) #:yield: +sec+ return yield(sec) if sec.nil? or sec.zero? exception = klass || Class.new(Timeout::Error) state_lock = Mutex.new state = :sleeping begin begin x = Thread.current y = Thread.start { begin sleep sec rescue => e state_lock.synchronize do if state == :sleeping state = :exception x.raise e end end else state_lock.synchronize do if state == :sleeping state = :timeout x.raise exception, "execution expired" end end end } rv = yield(sec) state_lock.synchronize do if state == :sleeping state = :returned_from_yield return rv end end ensure state_lock.synchronize do if state == :sleeping || state == :returned_from_yield state = :killing if y y.kill y.join # make sure y is dead. end end end end rescue exception => e rej = /\A#{Regexp.quote(__FILE__)}:#{__LINE__ - 4}\z/o (bt = e.backtrace).reject! { |m| rej =~ m } level = -caller(CALLER_OFFSET).size while THIS_FILE =~ bt[level] bt.delete_at(level) level += 1 end raise if klass # if exception class is specified, it # would be expected outside. raise Error, e.message, e.backtrace end end