Class: Complyance::RetryStrategy

Inherits:
Object
  • Object
show all
Defined in:
lib/complyance/retry_strategy.rb

Overview

Advanced retry strategy with exponential backoff, jitter, and circuit breaker

Instance Method Summary collapse

Constructor Details

#initialize(config = {}) ⇒ RetryStrategy

Initialize retry strategy

Parameters:

  • config (Hash) (defaults to: {})

    Retry configuration

Options Hash (config):

  • :max_attempts (Integer)

    Maximum number of retry attempts

  • :base_delay (Integer)

    Base delay in milliseconds

  • :max_delay (Integer)

    Maximum delay in milliseconds

  • :backoff_multiplier (Float)

    Multiplier for exponential backoff

  • :jitter_factor (Float)

    Random jitter factor (0-1)

  • :retryable_errors (Array<String>)

    List of error codes to retry

  • :retryable_http_codes (Array<Integer>)

    List of HTTP status codes to retry

  • :circuit_breaker_enabled (Boolean)

    Whether to use circuit breaker

  • :circuit_breaker_config (Hash)

    Circuit breaker configuration



20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# File 'lib/complyance/retry_strategy.rb', line 20

def initialize(config = {})
  @config = {
    max_attempts: 3,
    base_delay: 1000,
    max_delay: 30000,
    backoff_multiplier: 2.0,
    jitter_factor: 0.2,
    retryable_errors: ['RATE_LIMIT_EXCEEDED', 'SERVICE_UNAVAILABLE'],
    retryable_http_codes: [408, 429, 500, 502, 503, 504],
    circuit_breaker_enabled: true,
    circuit_breaker_config: {
      failure_threshold: 3,
      reset_timeout: 60
    }
  }.merge(config)

  @logger = Logger.new(STDOUT)
  @logger.level = Logger::INFO

  if @config[:circuit_breaker_enabled]
    @circuit_breaker = CircuitBreaker.new(@config[:circuit_breaker_config])
  end
end

Instance Method Details

#circuit_breaker_stateString?

Get the current circuit breaker state (for monitoring)

Returns:

  • (String, nil)

    Current circuit breaker state



123
124
125
# File 'lib/complyance/retry_strategy.rb', line 123

def circuit_breaker_state
  @circuit_breaker&.state
end

#circuit_breaker_statsString

Get circuit breaker statistics (for monitoring)

Returns:

  • (String)

    Circuit breaker stats



129
130
131
132
133
134
135
136
# File 'lib/complyance/retry_strategy.rb', line 129

def circuit_breaker_stats
  if @circuit_breaker
    "CircuitBreaker{state=#{@circuit_breaker.state}, failures=#{@circuit_breaker.failure_count}, " \
    "lastFailure=#{@circuit_breaker.last_failure_time}}"
  else
    "Circuit breaker disabled"
  end
end

#execute(operation_name, &operation) ⇒ Object

Execute a function with retry logic

Parameters:

  • operation (Proc)

    Function to execute

  • operation_name (String)

    Name of operation for logging

Returns:

  • (Object)

    Result of operation

Raises:

  • (SDKException)

    If all retries fail



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
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/complyance/retry_strategy.rb', line 49

def execute(operation_name, &operation)
  attempt = 1
  last_exception = nil

  while attempt <= @config[:max_attempts]
    begin
      @logger.debug "Attempting operation '#{operation_name}' (attempt #{attempt}/#{@config[:max_attempts]})"

      # Use circuit breaker if enabled
      result = if @circuit_breaker
        begin
          @circuit_breaker.execute(&operation)
        rescue RuntimeError => e
          if e.message.include?('Circuit breaker is open')
            raise SDKException.new(ErrorDetail.new(
              'CIRCUIT_BREAKER_OPEN',
              e.message
            ))
          end
          raise e
        end
      else
        operation.call
      end

      if attempt > 1
        @logger.info "Operation '#{operation_name}' succeeded on attempt #{attempt}"
      end

      return result

    rescue SDKException => e
      last_exception = e

      # Check if this error should be retried
      unless should_retry?(e, attempt)
        @logger.debug "Operation '#{operation_name}' failed with non-retryable error: #{e.message}"
        raise e
      end

      # If this was the last attempt, don't wait
      break if attempt >= @config[:max_attempts]

      # Calculate delay and wait
      delay = calculate_delay(attempt)
      @logger.warn "Operation '#{operation_name}' failed on attempt #{attempt} (#{e.message}), retrying in #{delay}ms"

      sleep(delay / 1000.0) # Convert ms to seconds

    rescue StandardError => e
      @logger.error "Unexpected error in operation '#{operation_name}': #{e.message}"

      error = ErrorDetail.new(
        'PROCESSING_ERROR',
        "Unexpected error: #{e.message}"
      )
      error.suggestion = "This appears to be an unexpected error. Please contact support if it persists"
      error.add_context_value('originalException', e.class.name)
      raise SDKException.new(error)
    end

    attempt += 1
  end

  # All retries exhausted
  @logger.error "Operation '#{operation_name}' failed after #{@config[:max_attempts]} attempts"
  raise last_exception || SDKException.new(ErrorDetail.new(
    'MAX_RETRIES_EXCEEDED',
    "Maximum retry attempts (#{@config[:max_attempts]}) exceeded"
  ))
end