Class: CMDx::Retry

Inherits:
Object
  • Object
show all
Defined in:
lib/cmdx/retry.rb

Overview

Configurable retry-on-exception wrapper around a task’s ‘work`. Supports exception list, attempt `:limit`, base `:delay`, `:max_delay` cap, and `:jitter` strategy (symbol, proc, or a configured block). Declared via `Task.retry_on` and accumulates across inheritance.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(exceptions, options = EMPTY_HASH, &block) {|attempt, delay, prev_delay| ... } ⇒ Retry

Returns a new instance of Retry.

Parameters:

  • exceptions (Array<Class>)

    exceptions to retry on

  • options (Hash{Symbol => Object}) (defaults to: EMPTY_HASH)
  • block (#call, nil)

    optional jitter callable used when ‘:jitter` isn’t set

Options Hash (options):

  • :limit (Integer) — default: 3

    maximum retry attempts

  • :delay (Float) — default: 0.5

    base delay in seconds between attempts

  • :max_delay (Float)

    clamp for computed delays

  • :jitter (Symbol, Proc, #call)

    built-in strategy (‘:exponential`, `:half_random`, `:full_random`, `:bounded_random`, `:linear`, `:fibonacci`, `:decorrelated_jitter`) or custom

Yield Parameters:

  • attempt (Integer)
  • delay (Float)
  • prev_delay (Float, nil)


24
25
26
27
28
# File 'lib/cmdx/retry.rb', line 24

def initialize(exceptions, options = EMPTY_HASH, &block)
  @exceptions = exceptions.flatten
  @options    = options.freeze
  @block      = block
end

Instance Attribute Details

#exceptionsObject (readonly)

Returns the value of attribute exceptions.



10
11
12
# File 'lib/cmdx/retry.rb', line 10

def exceptions
  @exceptions
end

Instance Method Details

#build(new_exceptions, new_options, &block) {|attempt, delay, prev_delay| ... } ⇒ Retry

Returns a new Retry layering ‘new_exceptions` and `new_options` onto the current one. Used for inheritance so subclasses extend rather than replace. Returns `self` only when every override (exceptions, options, and block) is empty so option-only updates such as `retry_on(limit: 5)` still take effect.

Parameters:

  • new_exceptions (Array<Class>)
  • new_options (Hash{Symbol => Object})
  • block (#call, nil)

    replacement jitter callable (falls back to the prior block)

Yields:

  • (attempt, delay, prev_delay)

    optional replacement jitter block

Returns:



41
42
43
44
45
46
47
48
# File 'lib/cmdx/retry.rb', line 41

def build(new_exceptions, new_options, &block)
  return self if new_exceptions.empty? && new_options.empty? && block.nil?

  merged_exceptions = exceptions | new_exceptions.flatten
  merged_options    = @options.merge(new_options)

  self.class.new(merged_exceptions, merged_options, &block || @block)
end

#delayFloat

Returns base delay in seconds.

Returns:

  • (Float)

    base delay in seconds



56
57
58
# File 'lib/cmdx/retry.rb', line 56

def delay
  @options[:delay] || 0.5
end

#jitterSymbol, ...

Returns jitter strategy or the block given to #initialize.

Returns:

  • (Symbol, Proc, #call, nil)

    jitter strategy or the block given to #initialize



66
67
68
# File 'lib/cmdx/retry.rb', line 66

def jitter
  @options[:jitter] || @block
end

#limitInteger

Returns:

  • (Integer)


51
52
53
# File 'lib/cmdx/retry.rb', line 51

def limit
  @options[:limit] || 3
end

#max_delayFloat?

Returns upper bound for computed delays.

Returns:

  • (Float, nil)

    upper bound for computed delays



61
62
63
# File 'lib/cmdx/retry.rb', line 61

def max_delay
  @options[:max_delay]
end

#process(task = nil) {|attempt| ... } ⇒ Object

Executes the block up to ‘limit + 1` times. Re-raises the last exception when attempts are exhausted.

Parameters:

  • task (Task, nil) (defaults to: nil)

    passed to #wait so jitter strategies can use it

Yield Parameters:

  • attempt (Integer)

    zero-based attempt index

Yield Returns:

  • (Object)

    the block’s successful return value

Returns:

  • (Object)

    the block’s successful return value

Raises:

  • (Exception)

    the last caught exception once retries exhaust



127
128
129
130
131
132
133
134
135
136
137
138
139
# File 'lib/cmdx/retry.rb', line 127

def process(task = nil, &)
  return yield(0) if exceptions.empty? || !limit.positive?

  prev_delay = nil
  (limit + 1).times do |attempt|
    return yield(attempt)
  rescue *exceptions => e
    raise(e) if attempt >= limit
    raise(e) unless Util.satisfied?(@options[:if], @options[:unless], task, e, attempt)

    prev_delay = wait(attempt, task, prev_delay)
  end
end

#wait(attempt, task = nil, prev_delay = nil) ⇒ Float?

Sleeps ‘attempt`’s jittered/bounded delay. No-op when the base delay is zero.

Custom jitter callables (registry, task Symbol method, ‘Proc` / block via `instance_exec` on the task, and other `#call`-ables) always receive `(attempt, delay, prev_delay)` so strategies share one shape; ignore `prev_delay` when you do not need decorrelated threading.

Non-numeric or non-finite jitter results are sanitized to the base ‘delay` and the final sleep is always clamped to `[0, max_delay]` when `max_delay` is set, preventing self-DoS from a buggy jitter returning `Float::INFINITY` or a non-Numeric value.

Parameters:

  • attempt (Integer)

    zero-based retry attempt number

  • task (Task, nil) (defaults to: nil)

    used as receiver for Symbol/Proc jitter strategies

  • prev_delay (Float, nil) (defaults to: nil)

    previous computed delay; only consumed by ‘:decorrelated_jitter` to thread state across attempts

Returns:

  • (Float, nil)

    the computed (and possibly clamped) delay, or ‘nil` when `delay` is zero



88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
# File 'lib/cmdx/retry.rb', line 88

def wait(attempt, task = nil, prev_delay = nil)
  return unless delay.positive?

  d =
    case jitter
    when NilClass
      delay
    when Symbol
      registry = retriers_registry(task)

      if registry.key?(jitter)
        registry.lookup(jitter).call(attempt, delay, prev_delay)
      else
        task.send(jitter, attempt, delay, prev_delay)
      end
    when Proc
      task.instance_exec(attempt, delay, prev_delay, &jitter)
    else
      if jitter.respond_to?(:call)
        jitter.call(attempt, delay, prev_delay)
      else
        delay
      end
    end

  d = delay unless d.is_a?(Numeric) && d.finite?
  d = d.clamp(0, max_delay) if max_delay
  Kernel.sleep(d) if d.positive?
  d
end