Module: SafeRequestTimeout

Defined in:
lib/safe_request_timeout.rb,
lib/safe_request_timeout/hooks.rb,
lib/safe_request_timeout/railtie.rb,
lib/safe_request_timeout/version.rb,
lib/safe_request_timeout/rack_middleware.rb,
lib/safe_request_timeout/active_record_hook.rb,
lib/safe_request_timeout/sidekiq_middleware.rb

Overview

This module adds the capability to add a general timeout to any block of code. Unlike the Timeout module, an error is not raised to indicate a timeout. Instead, the timed_out? method can be used to check if the block of code has taken longer than the specified duration so the application can take the appropriate action.

This is a safer alternative to the Timeout module because it does not fork new threads or risk raising errors from unexpected places.

Examples:

SafeRequestTimeout.timeout(5) do
  # calling check_timeout! will raise an error if the block has taken
  # longer than 5 seconds to execute.
  SafeRequestTimeout.check_timeout!
end

Defined Under Namespace

Modules: Hooks Classes: ActiveRecordHook, RackMiddleware, Railtie, SidekiqMiddleware, TimeoutError

Constant Summary collapse

VERSION =
File.read(File.expand_path("../../VERSION", __dir__)).chomp.freeze

Class Method Summary collapse

Class Method Details

.check_timeout!void

This method returns an undefined value.

Raise an error if the current timeout block has timed out. If there is no timeout block, then this method does nothing. If an error is raised, then the current timeout is cleared to prevent the error from being raised multiple times.

Raises:



74
75
76
77
78
79
80
81
# File 'lib/safe_request_timeout.rb', line 74

def check_timeout!
  if timed_out?
    state = current_state
    state[:safe_request_timeout_fired_at] = state[:safe_request_timeout_timeout_at]
    state[:safe_request_timeout_timeout_at] = nil
    raise TimeoutError.new("after #{time_elapsed.round(6)} seconds")
  end
end

.clear_timeout { ... } ⇒ Object

Clear the current timeout. If a block is passed, then the timeout will be cleared only for the duration of the block.

Yields:

  • the block to execute if one is given

Yield Returns:

  • (Object)

    the result of the block



126
127
128
129
130
131
132
# File 'lib/safe_request_timeout.rb', line 126

def clear_timeout(&block)
  if block
    timeout(nil, &block)
  else
    set_timeout(nil)
  end
end

.set_timeout(duration) ⇒ void

This method returns an undefined value.

Set the duration for the current timeout block. This is useful if you want to set the duration after the timeout block has started. The timer for the timeout block will restart whenever a new duration is set.



106
107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/safe_request_timeout.rb', line 106

def set_timeout(duration)
  state = current_state
  if state[:safe_request_timeout_started_at]
    start_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
    duration = duration.call if duration.respond_to?(:call)
    timeout_at = start_at + duration if duration
    state[:safe_request_timeout_started_at] = start_at
    state[:safe_request_timeout_timeout_at] = timeout_at
    # Arming a new deadline invalidates any record of a previously fired deadline so
    # that the ensure block in `timeout` cannot mistake the new deadline for one that
    # has already raised.
    state[:safe_request_timeout_fired_at] = nil if timeout_at
  end
end

.time_elapsedFloat?

Get the number of seconds elapsed in the current timeout block or nil if there is no timeout block.

Returns:

  • (Float, nil)

    the number of seconds elapsed in the current timeout block began



96
97
98
99
# File 'lib/safe_request_timeout.rb', line 96

def time_elapsed
  start_at = current_state[:safe_request_timeout_started_at]
  Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_at if start_at
end

.time_remainingFloat?

Get the number of seconds remaining in the current timeout block or nil if there is no timeout block.

Returns:

  • (Float, nil)

    the number of seconds remaining in the current timeout block



87
88
89
90
# File 'lib/safe_request_timeout.rb', line 87

def time_remaining
  timeout_at = current_state[:safe_request_timeout_timeout_at]
  [timeout_at - Process.clock_gettime(Process::CLOCK_MONOTONIC), 0.0].max if timeout_at
end

.timed_out?Boolean

Check if the current timeout block has timed out.

Returns:

  • (Boolean)

    true if the current timeout block has timed out



63
64
65
66
# File 'lib/safe_request_timeout.rb', line 63

def timed_out?
  timeout_at = current_state[:safe_request_timeout_timeout_at]
  !!timeout_at && Process.clock_gettime(Process::CLOCK_MONOTONIC) > timeout_at
end

.timeout(duration) { ... } ⇒ Object

Execute the given block with a timeout. If the block takes longer than the specified duration to execute, then the `timed_out?`` method will return true within the block.

Parameters:

  • duration (Integer)

    the number of seconds to wait before timing out

Yields:

  • the block to execute

Yield Returns:

  • (Object)

    the result of the block



31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# File 'lib/safe_request_timeout.rb', line 31

def timeout(duration, &block)
  duration = duration.call if duration.respond_to?(:call)

  state = current_state
  previous_start_at = state[:safe_request_timeout_started_at]
  previous_timeout_at = state[:safe_request_timeout_timeout_at]

  start_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
  timeout_at = start_at + duration if duration
  if timeout_at && previous_timeout_at && previous_timeout_at < timeout_at
    timeout_at = previous_timeout_at
  end

  begin
    state[:safe_request_timeout_started_at] = start_at
    state[:safe_request_timeout_timeout_at] = timeout_at
    yield
  ensure
    # If the deadline shared with the parent block was already raised by check_timeout!,
    # don't re-arm it when restoring the parent block's state.
    if previous_timeout_at && previous_timeout_at == state[:safe_request_timeout_fired_at]
      previous_timeout_at = nil
    end
    state[:safe_request_timeout_started_at] = previous_start_at
    state[:safe_request_timeout_timeout_at] = previous_timeout_at
    state[:safe_request_timeout_fired_at] = nil if previous_start_at.nil?
  end
end