Class: Lutaml::Hal::RateLimiter

Inherits:
Object
  • Object
show all
Defined in:
lib/lutaml/hal/rate_limiter.rb

Constant Summary collapse

DEFAULT_MAX_RETRIES =
5
DEFAULT_BASE_DELAY =
0.05
DEFAULT_MAX_DELAY =
5.0
DEFAULT_BACKOFF_FACTOR =
1.5
DEFAULT_MAX_RETRY_AFTER =

Separate, far larger cap for server-instructed waits. max_delay bounds our own guesswork; when the server states how long to wait, honouring it is the point -- capping a Retry-After: 60 at a 5s backoff cap would hammer an API that explicitly asked us to back off. This cap exists only to stop an absurd value (Retry-After: 3600) parking a worker thread.

300.0

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(options = {}) ⇒ RateLimiter

Returns a new instance of RateLimiter.



21
22
23
24
25
26
27
28
29
30
31
# File 'lib/lutaml/hal/rate_limiter.rb', line 21

def initialize(options = {})
  @max_retries = options[:max_retries] || DEFAULT_MAX_RETRIES
  @base_delay = options[:base_delay] || DEFAULT_BASE_DELAY
  @max_delay = options[:max_delay] || DEFAULT_MAX_DELAY
  @backoff_factor = options[:backoff_factor] || DEFAULT_BACKOFF_FACTOR
  @max_retry_after = options[:max_retry_after] || DEFAULT_MAX_RETRY_AFTER
  @enabled = options[:enabled] != false
  # Off by default: 403 usually means "not allowed", not "slow down".
  # Some APIs (historically the W3C API) use it as a rate-limit signal.
  @retry_on_forbidden = options[:retry_on_forbidden] ? true : false
end

Instance Attribute Details

#backoff_factorObject (readonly)

Returns the value of attribute backoff_factor.



19
20
21
# File 'lib/lutaml/hal/rate_limiter.rb', line 19

def backoff_factor
  @backoff_factor
end

#base_delayObject (readonly)

Returns the value of attribute base_delay.



19
20
21
# File 'lib/lutaml/hal/rate_limiter.rb', line 19

def base_delay
  @base_delay
end

#max_delayObject (readonly)

Returns the value of attribute max_delay.



19
20
21
# File 'lib/lutaml/hal/rate_limiter.rb', line 19

def max_delay
  @max_delay
end

#max_retriesObject (readonly)

Returns the value of attribute max_retries.



19
20
21
# File 'lib/lutaml/hal/rate_limiter.rb', line 19

def max_retries
  @max_retries
end

#max_retry_afterObject (readonly)

Returns the value of attribute max_retry_after.



19
20
21
# File 'lib/lutaml/hal/rate_limiter.rb', line 19

def max_retry_after
  @max_retry_after
end

Instance Method Details

#calculate_delay(attempt, error = nil) ⇒ Object



60
61
62
63
64
65
66
67
68
69
70
# File 'lib/lutaml/hal/rate_limiter.rb', line 60

def calculate_delay(attempt, error = nil)
  # Retry-After is bounded at both ends: capped at max_retry_after so an
  # absurd value can't park the thread, floored at base_delay because a
  # `Retry-After: 0` (or a date already in the past) would otherwise
  # sleep(0) and burn the whole retry budget in a busy loop.
  retry_after = retry_after_from_error(error)
  return retry_after.clamp(@base_delay, @max_retry_after) if retry_after

  delay = @base_delay * (@backoff_factor**(attempt - 1))
  [delay, @max_delay].min
end

#disable!Object



93
94
95
# File 'lib/lutaml/hal/rate_limiter.rb', line 93

def disable!
  @enabled = false
end

#enable!Object



89
90
91
# File 'lib/lutaml/hal/rate_limiter.rb', line 89

def enable!
  @enabled = true
end

#enabled?Boolean

Returns:

  • (Boolean)


97
98
99
# File 'lib/lutaml/hal/rate_limiter.rb', line 97

def enabled?
  @enabled
end

#extract_retry_after(response) ⇒ Object



72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
# File 'lib/lutaml/hal/rate_limiter.rb', line 72

def extract_retry_after(response)
  headers = response[:headers] || {}
  retry_after = headers['retry-after'] || headers['Retry-After']
  return nil unless retry_after

  if retry_after.match?(/^\d+$/)
    retry_after.to_i
  else
    begin
      retry_time = Time.parse(retry_after)
      [retry_time - Time.now, 0].max
    rescue ArgumentError
      nil
    end
  end
end

#retry_on_forbidden?Boolean

Returns:

  • (Boolean)


56
57
58
# File 'lib/lutaml/hal/rate_limiter.rb', line 56

def retry_on_forbidden?
  @retry_on_forbidden
end

#should_retry?(error, attempt) ⇒ Boolean

Returns:

  • (Boolean)


49
50
51
52
53
54
# File 'lib/lutaml/hal/rate_limiter.rb', line 49

def should_retry?(error, attempt)
  return false if attempt > @max_retries
  return @retry_on_forbidden if error.is_a?(ForbiddenError)

  error.is_a?(TooManyRequestsError) || error.is_a?(ServerError)
end

#with_rate_limitingObject



33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# File 'lib/lutaml/hal/rate_limiter.rb', line 33

def with_rate_limiting
  return yield unless @enabled

  attempt = 0
  begin
    attempt += 1
    yield
  rescue TooManyRequestsError, ServerError, ForbiddenError => e
    raise unless should_retry?(e, attempt)

    delay = calculate_delay(attempt, e)
    sleep(delay)
    retry
  end
end