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| ... } ⇒ 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)


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

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| ... } ⇒ Retry

Returns a new Retry layering ‘new_exceptions` and `new_options` onto the current one. Used for inheritance so subclasses extend rather than replace.

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)

    optional replacement jitter block

Returns:



38
39
40
41
42
43
44
45
# File 'lib/cmdx/retry.rb', line 38

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

  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



53
54
55
# File 'lib/cmdx/retry.rb', line 53

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



63
64
65
# File 'lib/cmdx/retry.rb', line 63

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

#limitInteger

Returns:

  • (Integer)


48
49
50
# File 'lib/cmdx/retry.rb', line 48

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

#max_delayFloat?

Returns upper bound for computed delays.

Returns:

  • (Float, nil)

    upper bound for computed delays



58
59
60
# File 'lib/cmdx/retry.rb', line 58

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



113
114
115
116
117
118
119
120
121
122
123
124
125
# File 'lib/cmdx/retry.rb', line 113

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.

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



75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/cmdx/retry.rb', line 75

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)
      end
    when Proc
      task.instance_exec(attempt, delay, &jitter)
    else
      if jitter.respond_to?(:call)
        jitter.call(attempt, delay)
      else
        delay
      end
    end

  d = d.clamp(0, max_delay) if max_delay
  Kernel.sleep(d) if d.positive?
  d
end