Class: Lutaml::Hal::RateLimiter
- Inherits:
-
Object
- Object
- Lutaml::Hal::RateLimiter
- 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: 60at 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
-
#backoff_factor ⇒ Object
readonly
Returns the value of attribute backoff_factor.
-
#base_delay ⇒ Object
readonly
Returns the value of attribute base_delay.
-
#max_delay ⇒ Object
readonly
Returns the value of attribute max_delay.
-
#max_retries ⇒ Object
readonly
Returns the value of attribute max_retries.
-
#max_retry_after ⇒ Object
readonly
Returns the value of attribute max_retry_after.
Instance Method Summary collapse
- #calculate_delay(attempt, error = nil) ⇒ Object
- #disable! ⇒ Object
- #enable! ⇒ Object
- #enabled? ⇒ Boolean
- #extract_retry_after(response) ⇒ Object
-
#initialize(options = {}) ⇒ RateLimiter
constructor
A new instance of RateLimiter.
- #retry_on_forbidden? ⇒ Boolean
- #should_retry?(error, attempt) ⇒ Boolean
- #with_rate_limiting ⇒ Object
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( = {}) @max_retries = [:max_retries] || DEFAULT_MAX_RETRIES @base_delay = [:base_delay] || DEFAULT_BASE_DELAY @max_delay = [:max_delay] || DEFAULT_MAX_DELAY @backoff_factor = [:backoff_factor] || DEFAULT_BACKOFF_FACTOR @max_retry_after = [:max_retry_after] || DEFAULT_MAX_RETRY_AFTER @enabled = [: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 = [:retry_on_forbidden] ? true : false end |
Instance Attribute Details
#backoff_factor ⇒ Object (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_delay ⇒ Object (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_delay ⇒ Object (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_retries ⇒ Object (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_after ⇒ Object (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
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
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
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_limiting ⇒ Object
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 |