Class: R4r::Retry

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

Overview

Decorator that wrap blocks and call it within retries.

Examples:

constant backoff, it will never pause between retries and will try 3 times.

retry = R4r::Retry.constant_backoff(num_retries: 3)
retry.call { get_http_request }

exponential backoff, it will pause between invocations using given backoff invtervals and will try 4 times

retry = R4r::Retry.backoff(backoff: [0.1, 0.3, 1, 5])
retry.call { get_http_request }

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(backoff:, policy: nil, budget: nil) ⇒ Retry

Creates a new retries dectorator.

Parameters:

  • backoff (Array[Float])

    an array with backoff intervals (in seconds)

  • policy (R4r::RetryPolicy) (defaults to: nil)

    a policy used for error filtiring

  • budget (R4r::RetryBudget) (defaults to: nil)

    a retry budget

Raises:

  • (ArgumentError)

    when backoff is empty

  • (ArgumentError)

    when backoff has negative values



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

def initialize(backoff:, policy: nil, budget: nil)
  @policy = (policy || R4r::RetryPolicy.always)
  @backoff = Array.new(backoff).map { |i| i.to_f }
  @budget = budget != nil ? budget : R4r::RetryBudget.create

  raise ArgumentError, "backoff cannot be empty" if @backoff.empty?
  raise ArgumentError, "backoff values cannot be negative" unless @backoff.all? {|i| i.to_f >= 0.0 }
end

Instance Attribute Details

#backoffArray[Float]

Returns the current value of backoff.

Returns:

  • (Array[Float])

    the current value of backoff



29
30
31
# File 'lib/r4r/retry.rb', line 29

def backoff
  @backoff
end

#budgetR4r::RetryBudget

Returns the current value of budget.

Returns:



29
30
31
# File 'lib/r4r/retry.rb', line 29

def budget
  @budget
end

Class Method Details

.backoff(backoff:, policy: nil, budget: nil) ⇒ R4r::Retry

Creates a R4r::Retry with backoff intervals.

Examples:

exponential backoff between invocations

R4r::Retry.backoff(backoff: [0.1, 0.5, 1, 3])

Parameters:

  • backoff (Array[Float])

    a list of sleep intervals (in seconds)

  • policy (R4r::RetryPolicy) (defaults to: nil)

    a policy used for error filtiring

  • budget (R4r::RetryBudget) (defaults to: nil)

    a retry budget

Returns:

Raises:

  • (ArgumentError)

    when backoff is nil

  • (ArgumentError)

    when backoff isn’t array

  • (ArgumentError)

    when backoff has negative values



127
128
129
130
131
132
133
# File 'lib/r4r/retry.rb', line 127

def self.backoff(backoff:, policy: nil, budget: nil)
  raise ArgumentError, "backoff cannot be nil" if backoff.nil?
  raise ArgumentError, "backoff must be an array" unless backoff.is_a?(Array)
  raise ArgumentError, "backoff values cannot be negative" unless backoff.all? {|i| i.to_f >= 0.0 }

  R4r::Retry.new(backoff: backoff, policy: policy, budget: budget)
end

.constant_backoff(num_retries:, backoff: 0.0, policy: nil, budget: nil) ⇒ R4r::Retry

Creates a R4r::Retry with fixed backoff rates.

Examples:

without sleep between invocations

R4r::Retry.constant_backoff(num_retries:3)

with sleep 1s between invocations

R4r::Retry.constant_backoff(num_retries: 3, backoff: 1)

Parameters:

Returns:

Raises:

  • (ArgumentError)

    when num_retries is negative

  • (ArgumentError)

    when backoff is negative



106
107
108
109
110
111
112
# File 'lib/r4r/retry.rb', line 106

def self.constant_backoff(num_retries:, backoff: 0.0, policy: nil, budget: nil)
  raise ArgumentError, "num_retries cannot be negative" unless num_retries.to_i >= 0
  raise ArgumentError, "backoff cannot be negative" unless backoff.to_f >= 0.0

  backoff = Array.new(num_retries.to_i) { backoff.to_f }
  R4r::Retry.new(backoff: backoff, policy: policy, budget: budget)
end

Instance Method Details

#call(&block) ⇒ Object

Calls given block within retries.

Raises:



61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/r4r/retry.rb', line 61

def call(&block)
  return unless block_given?

  num_retry = 0
  @budget.deposit

  while num_retry < @backoff.size

    begin
      return yield(num_retry)
    rescue => err
      raise err if err.is_a?(NonRetriableError)

      if (num_retry + 1 == @backoff.size)
        raise NonRetriableError.new(message: "Retry limit [#{@backoff.size}] reached: #{err}", cause: err)
      end

      unless @policy.call(error: err, num_retry: num_retry)
        raise NonRetriableError.new(message: "An error was rejected by policy: #{err}", cause: err)
      end

      unless @budget.try_withdraw
        raise NonRetriableError.new(message: "Budget was exhausted: #{err}", cause: err)
      end
    end

    sleep @backoff[num_retry]
    num_retry += 1
  end
end

#decorate(&block) ⇒ Proc

Decorates a given block within retries.

Returns:

  • (Proc)


54
55
56
# File 'lib/r4r/retry.rb', line 54

def decorate(&block)
  ->() { call { yield } }
end