@@ -22,7 +22,7 @@ module Timeout
2222 # The version
2323 VERSION = "0.5.0"
2424
25- # Internal error raised to when a timeout is triggered.
25+ # Internal exception raised to when a timeout is triggered.
2626 class ExitException < Exception
2727 def exception ( *) # :nodoc:
2828 self
@@ -177,7 +177,7 @@ def finished
177177
178178 # :startdoc:
179179
180- # Perform an operation in a block, raising an error if it takes longer than
180+ # Perform an operation in a block, raising an exception if it takes longer than
181181 # +sec+ seconds to complete.
182182 #
183183 # +sec+:: Number of seconds to wait for the block to terminate. Any non-negative number
@@ -190,19 +190,64 @@ def finished
190190 # Omitting will use the default, "execution expired"
191191 #
192192 # Returns the result of the block *if* the block completed before
193- # +sec+ seconds, otherwise throws an exception, based on the value of +klass+.
193+ # +sec+ seconds, otherwise raises an exception, based on the value of +klass+.
194194 #
195- # The exception thrown to terminate the given block cannot be rescued inside
196- # the block unless +klass+ is given explicitly. However, the block can use
197- # ensure to prevent the handling of the exception. For that reason, this
198- # method cannot be relied on to enforce timeouts for untrusted blocks.
195+ # The exception raised to terminate the given block is the given +klass+, or
196+ # Timeout::ExitException if +klass+ is not given. The reason for that behavior
197+ # is that Timeout::Error inherits from RuntimeError and might be caught unexpectedly by `rescue`.
198+ # Timeout::ExitException inherits from Exception so it will only be rescued by `rescue Exception`.
199+ # Note that the Timeout::ExitException is translated to a Timeout::Error once it reaches the Timeout.timeout call,
200+ # so outside that call it will be a Timeout::Error.
201+ #
202+ # In general, be aware that the code block may rescue the exception, and in such a case not respect the timeout.
203+ # Also, the block can use +ensure+ to prevent the handling of the exception.
204+ # For those reasons, this method cannot be relied on to enforce timeouts for untrusted blocks.
199205 #
200206 # If a scheduler is defined, it will be used to handle the timeout by invoking
201207 # Scheduler#timeout_after.
202208 #
203209 # Note that this is both a method of module Timeout, so you can <tt>include
204210 # Timeout</tt> into your classes so they have a #timeout method, as well as
205211 # a module method, so you can call it directly as Timeout.timeout().
212+ #
213+ # ==== Ensuring the exception does not fire inside ensure blocks
214+ #
215+ # When using Timeout.timeout it can be desirable to ensure the timeout exception does not fire inside an +ensure+ block.
216+ # The simplest and best way to do so it to put the Timeout.timeout call inside the body of the begin/ensure/end:
217+ #
218+ # begin
219+ # Timeout.timeout(sec) { some_long_operation }
220+ # ensure
221+ # cleanup # safe, cannot be interrupt by timeout
222+ # end
223+ #
224+ # If that is not feasible, e.g. if there are +ensure+ blocks inside +some_long_operation+,
225+ # they need to not be interrupted by timeout, and it's not possible to move these ensure blocks outside,
226+ # one can use Thread.handle_interrupt to delay the timeout exception like so:
227+ #
228+ # Thread.handle_interrupt(Timeout::Error => :never) {
229+ # Timeout.timeout(sec, Timeout::Error) do
230+ # setup # timeout cannot happen here, no matter how long it takes
231+ # Thread.handle_interrupt(Timeout::Error => :immediate) {
232+ # some_long_operation # timeout can happen here
233+ # }
234+ # ensure
235+ # cleanup # timeout cannot happen here, no matter how long it takes
236+ # end
237+ # }
238+ #
239+ # An important thing to note is the need to pass an exception klass to Timeout.timeout,
240+ # otherwise it does not work. Specifically, using +Thread.handle_interrupt(Timeout::ExitException => ...)+
241+ # is unsupported and causes subtle errors like raising the wrong exception outside the block, do not use that.
242+ #
243+ # Note that Thread.handle_interrupt is somewhat dangerous because if setup or cleanup hangs
244+ # then the current thread will hang too and the timeout will never fire.
245+ # Also note the block might run for longer than +sec+ seconds:
246+ # e.g. some_long_operation executes for +sec+ seconds + whatever time cleanup takes.
247+ #
248+ # If you want the timeout to only happen on blocking operations one can use :on_blocking
249+ # instead of :immediate. However, that means if the block uses no blocking operations after +sec+ seconds,
250+ # the block will not be interrupted.
206251 def self . timeout ( sec , klass = nil , message = nil , &block ) #:yield: +sec+
207252 return yield ( sec ) if sec == nil or sec . zero?
208253 raise ArgumentError , "Timeout sec must be a non-negative number" if 0 > sec
0 commit comments