Module: R2::Retry

Defined in:
lib/r2/retry.rb

Overview

Automatic retries with exponential backoff.

Transient failures, such as brief network instabilities, are retried a limited number of times. The wait between the attempts grows exponentially and is capped, so the remote service has time to recover without making the user wait indefinitely.

Only the error classes informed as transient are retried; every other failure is raised immediately, preserving the domain error mapping.

Defined Under Namespace

Classes: Policy

Constant Summary collapse

DEFAULT_MAX_ATTEMPTS =

Total number of attempts: the first execution plus the retries.

3
DEFAULT_BASE_DELAY =

Wait applied after the first failed attempt, in seconds.

0.5
DEFAULT_MAX_DELAY =

Upper limit of the wait between attempts, in seconds.

5.0
DEFAULT_SLEEPER =

Waiting strategy used when nothing else is provided.

->(seconds) { sleep(seconds) }
DEFAULT_DESCRIPTION =

Description used in the diagnostics when the caller gives none.

"operation"

Class Method Summary collapse

Class Method Details

.call(policy: Policy.new, retry_on: [], logger: nil, sleeper: nil, description: nil) { ... } ⇒ Object

Runs the block, retrying the transient failures it raises.

Parameters:

  • policy (Policy) (defaults to: Policy.new) —

    retry settings

  • retry_on (Array<Class>) (defaults to: []) —

    error classes considered transient

  • logger (#debug, nil) (defaults to: nil) —

    logger used for diagnostics

  • sleeper (#call, nil) (defaults to: nil) —

    waiting strategy used between attempts

  • description (String, nil) (defaults to: nil) —

    operation description used in the diagnostics

Yields:

  • the operation to run

Returns:

  • (Object) —

    result of the block

Raises:

  • (StandardError) —

    error of the last attempt when the retries are exhausted



65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# File 'lib/r2/retry.rb', line 65

def self.call(policy: Policy.new, retry_on: [], logger: nil, sleeper: nil, description: nil)
    attempt = 0
    description ||= DEFAULT_DESCRIPTION

    begin
        attempt += 1
        yield
    rescue StandardError => e
        raise unless transient?(e, retry_on) && attempt < policy.max_attempts

        delay = policy.delay_for(attempt)
        logger&.debug(retry_message(attempt, policy, description, e, delay))
        (sleeper || DEFAULT_SLEEPER).call(delay)
        retry
    end
end

.retry_message(attempt, policy, description, error, delay) ⇒ String

Builds the diagnostic message of a retry.

Parameters:

  • attempt (Integer) —

    number of the attempt that just failed

  • policy (Policy) —

    retry settings

  • description (String) —

    description of the operation

  • error (StandardError) —

    error raised by the attempt

  • delay (Float) —

    seconds to wait before the next attempt

Returns:

  • (String) —

    message describing the retry



99
100
101
102
103
# File 'lib/r2/retry.rb', line 99

def self.retry_message(attempt, policy, description, error, delay)
    "Retrying #{description} in #{delay}s (attempt #{attempt + 1} of " \
        "#{policy.max_attempts}) after a transient failure: " \
        "#{error.class}: #{error.message}"
end

.transient?(error, retry_on) ⇒ Boolean

Indicates whether an error is considered transient.

Parameters:

  • error (StandardError) —

    error raised by the operation

  • retry_on (Array<Class>) —

    error classes considered transient

Returns:

  • (Boolean) —

    true when the failure may be retried



87
88
89
# File 'lib/r2/retry.rb', line 87

def self.transient?(error, retry_on)
    retry_on.any? { |error_class| error.is_a?(error_class) }
end