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