Module: Aidp::Concurrency::Backoff

Defined in:
lib/aidp/concurrency/backoff.rb

Overview

Retry logic with exponential backoff and jitter.

Replaces ad-hoc retry loops that use sleep() with a standardized, configurable retry mechanism that includes backoff strategies and jitter.

Examples:

Simple retry

Backoff.retry(max_attempts: 5) { call_external_api() }

Custom backoff strategy

Backoff.retry(max_attempts: 10, base: 1.0, strategy: :linear) do
  unstable_operation()
end

With error filtering

Backoff.retry(max_attempts: 3, on: [Net::ReadTimeout, Errno::ECONNREFUSED]) do
  http_request()
end

Class Method Summary collapse

Class Method Details

.calculate_delay(attempt, strategy, base, max_delay, jitter) ⇒ Float

Calculate backoff delay for a given attempt.

Parameters:

  • attempt (Integer)

    Current attempt number (1-indexed)

  • strategy (Symbol)

    :exponential, :linear, or :constant

  • base (Float)

    Base delay in seconds

  • max_delay (Float)

    Maximum delay cap

  • jitter (Float)

    Jitter factor 0.0-1.0

Returns:

  • (Float)

    Delay in seconds



94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
# File 'lib/aidp/concurrency/backoff.rb', line 94

def calculate_delay(attempt, strategy, base, max_delay, jitter)
  delay = case strategy
  when :exponential
    base * (2**(attempt - 1))
  when :linear
    base * attempt
  when :constant
    base
  else
    raise ArgumentError, "Unknown strategy: #{strategy}"
  end

  # Cap at max_delay
  delay = [delay, max_delay].min

  # Add jitter: randomize between (1-jitter)*delay and delay
  # e.g., with jitter=0.2, delay is reduced by 0-20%
  if jitter > 0
    jitter_amount = delay * jitter * rand
    delay -= jitter_amount
  end

  delay
end

.retry(max_attempts: nil, base: nil, max_delay: nil, jitter: nil, strategy: :exponential, on: [StandardError]) { ... } ⇒ Object

Retry a block with exponential backoff and jitter.

Examples:

result = Backoff.retry(max_attempts: 5, base: 0.5, jitter: 0.2) do
  api_client.fetch_data
end

Parameters:

  • max_attempts (Integer) (defaults to: nil)

    Maximum number of attempts (default: from config)

  • base (Float) (defaults to: nil)

    Base delay in seconds (default: from config)

  • max_delay (Float) (defaults to: nil)

    Maximum delay between retries (default: from config)

  • jitter (Float) (defaults to: nil)

    Jitter factor 0.0-1.0 (default: from config)

  • strategy (Symbol) (defaults to: :exponential)

    Backoff strategy :exponential, :linear, or :constant

  • on (Array<Class>) (defaults to: [StandardError])

    Array of exception classes to retry (default: StandardError)

Yields:

  • Block to retry

Returns:

  • (Object)

    Result of the block

Raises:



42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
# File 'lib/aidp/concurrency/backoff.rb', line 42

def retry(max_attempts: nil, base: nil, max_delay: nil, jitter: nil,
  strategy: :exponential, on: [StandardError], &block)
  max_attempts ||= Concurrency.configuration.default_max_attempts
  base ||= Concurrency.configuration.default_backoff_base
  max_delay ||= Concurrency.configuration.default_backoff_max
  jitter ||= Concurrency.configuration.default_jitter

  raise ArgumentError, "Block required" unless block_given?
  raise ArgumentError, "max_attempts must be >= 1" if max_attempts < 1

  on = Array(on)
  last_error = nil
  attempt = 0

  while attempt < max_attempts
    attempt += 1

    begin
      result = block.call
      log_retry_success(attempt) if attempt > 1
      return result
    rescue => e
      last_error = e

      # Re-raise if error is not in the retry list
      raise unless on.any? { |klass| e.is_a?(klass) }

      # Re-raise on last attempt
      if attempt >= max_attempts
        log_max_attempts_exceeded(attempt, e)
        raise Concurrency::MaxAttemptsError, "Max attempts (#{max_attempts}) exceeded: #{e.class} - #{e.message}"
      end

      # Calculate delay and wait
      delay = calculate_delay(attempt, strategy, base, max_delay, jitter)
      log_retry_attempt(attempt, max_attempts, delay, e)
      sleep(delay) if delay > 0
    end
  end

  # Should never reach here, but just in case
  raise Concurrency::MaxAttemptsError, "Max attempts (#{max_attempts}) exceeded: #{last_error&.class} - #{last_error&.message}"
end