Class: Pvectl::Connection::RetryHandler

Inherits:
Object
  • Object
show all
Defined in:
lib/pvectl/connection/retry_handler.rb,
sig/pvectl/connection/retry_handler.rbs

Overview

Handles retry logic with exponential backoff for API requests.

RetryHandler wraps API calls to automatically retry on transient errors like network timeouts, server errors (5xx), and rate limiting (429). By default, only read operations (GET) are retried; write operations require explicit opt-in via retry_writes due to idempotency concerns.

Examples:

Basic usage with read operation

handler = RetryHandler.new(max_retries: 3, base_delay: 1, max_delay: 30)
result = handler.with_retry(method: :get) { api.nodes.get }

Write operation with retries disabled (default)

handler.with_retry(method: :post) { api.nodes[node].qemu.post(data) }
# Will NOT retry on failure

Write operation with retries enabled

handler = RetryHandler.new(max_retries: 3, base_delay: 1, max_delay: 30, retry_writes: true)
handler.with_retry(method: :post) { api.nodes[node].qemu.post(data) }
# Will retry on transient failures

Constant Summary collapse

RETRYABLE_EXCEPTIONS =

Exceptions that indicate transient failures safe to retry.

Includes:

  • Connection timeouts (OpenTimeout, ReadTimeout)
  • Server errors (5xx)
  • Rate limiting (429)
  • Network errors (ECONNREFUSED, ECONNRESET, SocketError)
  • Global timeout (Timeout::Error from Ruby's Timeout module)
[
  RestClient::Exceptions::OpenTimeout,
  RestClient::Exceptions::ReadTimeout,
  RestClient::InternalServerError,     # 500
  RestClient::BadGateway,              # 502
  RestClient::ServiceUnavailable,      # 503
  RestClient::GatewayTimeout,          # 504
  RestClient::TooManyRequests,         # 429
  Errno::ECONNREFUSED,
  Errno::ECONNRESET,
  SocketError,
  Timeout::Error
].freeze
READ_METHODS =

HTTP methods considered safe to retry (read-only, idempotent).

i[get head options].freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(max_retries:, base_delay:, max_delay:, retry_writes: false, logger: nil) ⇒ RetryHandler

Creates a new RetryHandler.



76
77
78
79
80
81
82
# File 'lib/pvectl/connection/retry_handler.rb', line 76

def initialize(max_retries:, base_delay:, max_delay:, retry_writes: false, logger: nil)
  @max_retries = max_retries
  @base_delay = base_delay
  @max_delay = max_delay
  @retry_writes = retry_writes
  @logger = logger
end

Instance Attribute Details

#base_delayNumeric (readonly)



58
59
60
# File 'lib/pvectl/connection/retry_handler.rb', line 58

def base_delay
  @base_delay
end

#loggerLogger? (readonly)



67
68
69
# File 'lib/pvectl/connection/retry_handler.rb', line 67

def logger
  @logger
end

#max_delayNumeric (readonly)



61
62
63
# File 'lib/pvectl/connection/retry_handler.rb', line 61

def max_delay
  @max_delay
end

#max_retriesInteger (readonly)



55
56
57
# File 'lib/pvectl/connection/retry_handler.rb', line 55

def max_retries
  @max_retries
end

#retry_writesBoolean (readonly)



64
65
66
# File 'lib/pvectl/connection/retry_handler.rb', line 64

def retry_writes
  @retry_writes
end

Instance Method Details

#calculate_delay(attempt) ⇒ Numeric

Calculates delay using exponential backoff.

Formula: delay = min(base_delay * 2^(attempt-1), max_delay)

Example with base_delay=1, max_delay=30:

  • Attempt 1: 1s
  • Attempt 2: 2s
  • Attempt 3: 4s
  • Attempt 4: 8s
  • Attempt 5+: capped at 30s


139
140
141
142
# File 'lib/pvectl/connection/retry_handler.rb', line 139

def calculate_delay(attempt)
  delay = base_delay * (2**(attempt - 1))
  [delay, max_delay].min
end

#log_retry(attempt, delay, error) ⇒ void

This method returns an undefined value.

Logs a retry attempt.

Security: Only logs error class name, NOT error.message which may contain sensitive data (URLs with tokens, request bodies, etc.)



152
153
154
155
156
157
158
# File 'lib/pvectl/connection/retry_handler.rb', line 152

def log_retry(attempt, delay, error)
  return unless logger

  logger.warn(
    "Retry #{attempt}/#{max_retries} after #{delay}s: #{error.class.name}"
  )
end

#should_retry?(method, attempts) ⇒ Boolean

Determines if the operation should be retried.



119
120
121
122
123
124
# File 'lib/pvectl/connection/retry_handler.rb', line 119

def should_retry?(method, attempts)
  return false if attempts > max_retries
  return true if READ_METHODS.include?(method)

  retry_writes
end

#with_retry(method: :get) { ... } ⇒ Object

Executes a block with retry logic.

Retries the block on transient errors using exponential backoff. By default, only read operations (GET, HEAD, OPTIONS) are retried. Write operations require retry_writes: true in the constructor.

Examples:

handler.with_retry(method: :get) { api.nodes.get }

Yields:

  • the API call to execute

Raises:

  • (Exception)

    the last error after all retries exhausted



97
98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/pvectl/connection/retry_handler.rb', line 97

def with_retry(method: :get)
  attempts = 0
  begin
    attempts += 1
    yield
  rescue *RETRYABLE_EXCEPTIONS => e
    raise unless should_retry?(method, attempts)

    delay = calculate_delay(attempts)
    log_retry(attempts, delay, e)
    sleep(delay)
    retry
  end
end