Module: Crimson::RetryHandler

Defined in:
lib/crimson/retry_handler.rb

Overview

Retry logic for API calls with exponential backoff and retry-After header support.

Constant Summary collapse

MAX_RETRIES =

Maximum number of retry attempts.

3
BASE_DELAY =

Base delay in seconds for exponential backoff.

1.0
MAX_DELAY =

Maximum delay cap in seconds.

30.0
RETRYABLE_MESSAGES =

Patterns in error messages that indicate a retry is appropriate.

[
  /rate.?limit/i,
  /too many requests/i,
  /429/,
  /5\d{2}/,
  /timeout/i,
  /timed?\s*out/i,
  /connection.*reset/i,
  /connection.*refused/i,
  /ECONNRESET/,
  /ECONNREFUSED/,
  /ETIMEDOUT/,
  /ENOTFOUND/,
  /network/i,
  /overloaded/i,
  /capacity/i,
  /server error/i,
  /service unavailable/i,
  /bad gateway/i,
  /gateway timeout/i,
  /internal server error/i
].freeze

Class Method Summary collapse

Class Method Details

.compute_delay(error, attempt, base_delay, max_delay) ⇒ Float

Compute delay using exponential backoff, respecting Retry-After headers.

Parameters:

  • error (StandardError)
  • attempt (Integer)

    current attempt number

  • base_delay (Float)
  • max_delay (Float)

Returns:

  • (Float)

    delay in seconds



77
78
79
80
81
82
83
# File 'lib/crimson/retry_handler.rb', line 77

def self.compute_delay(error, attempt, base_delay, max_delay)
  retry_after = extract_retry_after(error)
  return [retry_after, max_delay].min if retry_after && retry_after > 0

  delay = [base_delay * (2 ** (attempt - 1)), max_delay].min
  delay + rand * 0.5
end

.extract_retry_after(error) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Extract Retry-After header from an error response.



87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'lib/crimson/retry_handler.rb', line 87

def self.extract_retry_after(error)
  return nil unless error.respond_to?(:response)

  response = error.response
  return nil unless response.is_a?(Hash)

  headers = response[:headers] || response["headers"]
  return nil unless headers.is_a?(Hash)

  retry_after = headers["Retry-After"] || headers["retry-after"]
  return nil unless retry_after

  retry_after.to_f
rescue
  nil
end

.retryable?(error) ⇒ Boolean

Check whether an error is retryable based on its message.

Parameters:

  • error (StandardError)

Returns:

  • (Boolean)


66
67
68
69
# File 'lib/crimson/retry_handler.rb', line 66

def self.retryable?(error)
  message = "#{error.class}: #{error.message}"
  RETRYABLE_MESSAGES.any? { |pattern| message.match?(pattern) }
end

.with_retry(max_retries: MAX_RETRIES, base_delay: BASE_DELAY, max_delay: MAX_DELAY) { ... } ⇒ Object

Execute a block with retry logic.

Parameters:

  • max_retries (Integer) (defaults to: MAX_RETRIES)

    maximum retry attempts

  • base_delay (Float) (defaults to: BASE_DELAY)

    base delay in seconds

  • max_delay (Float) (defaults to: MAX_DELAY)

    maximum delay cap

Yields:

  • block to execute and potentially retry

Returns:

  • (Object)

    the result of the block

Raises:

  • (StandardError)

    if all retries are exhausted or error is non-retryable



44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# File 'lib/crimson/retry_handler.rb', line 44

def self.with_retry(max_retries: MAX_RETRIES, base_delay: BASE_DELAY, max_delay: MAX_DELAY)
  attempts = 0
  last_error = nil

  loop do
    attempts += 1
    begin
      return yield
    rescue => e
      last_error = e
      raise e if attempts > max_retries
      raise e unless retryable?(e)

      delay = compute_delay(e, attempts, base_delay, max_delay)
      sleep delay
    end
  end
end