Skip to content

Commit 7cfa5a6

Browse files
committed
Revise Timeout.timeout docs and add a section about ensure
1 parent a52720e commit 7cfa5a6

File tree

1 file changed

+52
-7
lines changed

1 file changed

+52
-7
lines changed

lib/timeout.rb

Lines changed: 52 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)