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; Task.retry_on with false selects Retry.disabled.

Instance Attribute Summary collapse

Class Method 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)


34
35
36
37
38
# File 'lib/cmdx/retry.rb', line 34

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

Instance Attribute Details

#exceptions ⇒ Object (readonly)

Returns the value of attribute exceptions.



20
21
22
# File 'lib/cmdx/retry.rb', line 20

def exceptions
  @exceptions
end

Class Method Details

.disabled ⇒ Retry

Returns disabled configuration (no exceptions).

Returns:

  • (Retry) —

    disabled configuration (no exceptions)



14
15
16
# File 'lib/cmdx/retry.rb', line 14

def disabled
  DISABLED
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:



51
52
53
54
55
56
57
58
# File 'lib/cmdx/retry.rb', line 51

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

#delay ⇒ Float

Returns base delay in seconds.

Returns:

  • (Float) —

    base delay in seconds



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

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

#enabled? ⇒ Boolean

Returns:

  • (Boolean)


81
82
83
# File 'lib/cmdx/retry.rb', line 81

def enabled?
  !exceptions.empty?
end

#jitter ⇒ Symbol, ...

Returns jitter strategy or the block given to #initialize.

Returns:

  • (Symbol, Proc, #call, nil) —

    jitter strategy or the block given to #initialize



76
77
78
# File 'lib/cmdx/retry.rb', line 76

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

#limit ⇒ Integer

Returns:

  • (Integer)


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

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

#max_delay ⇒ Float?

Returns upper bound for computed delays.

Returns:

  • (Float, nil) —

    upper bound for computed delays



71
72
73
# File 'lib/cmdx/retry.rb', line 71

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



142
143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/cmdx/retry.rb', line 142

def process(task = nil, &)
  return yield(0) if !enabled? || !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



103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/cmdx/retry.rb', line 103

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