Class: Rubino::Agent::BackoffPolicy
- Inherits:
-
Object
- Object
- Rubino::Agent::BackoffPolicy
- Defined in:
- lib/rubino/agent/backoff_policy.rb
Overview
Jittered exponential backoff for retries, a faithful port of the reference jittered_backoff:
delay = min(base * 2^(attempt-1), max)
result = delay + uniform(0, jitter_ratio * delay) # jitter_ratio = 0.5
Jitter decorrelates concurrent retries so multiple sessions hitting the same rate-limited provider don’t all retry at the same instant.
Deviation from the reference (intentional): the reference seeds a fresh RNG from a process-global monotonic counter + time on every call to stay decorrelated across threads with a coarse clock. We use Ruby’s ‘rand`, whose Mersenne-Twister default RNG is already per-process and well-distributed — no global counter, no lock, less code. The decorrelation property (jitter spread over [0, 0.5*delay]) is preserved.
Two presets mirror the conversation loop’s two backoff sites:
* INVALID_RESPONSE — base 5s, cap 120s
* ERROR_PATH — base 2s, cap 60s
Constant Summary collapse
- JITTER_RATIO =
0.5- INVALID_RESPONSE =
Preset = [base_delay, max_delay] in seconds.
{ base: 5.0, max: 120.0 }.freeze
- ERROR_PATH =
{ base: 2.0, max: 60.0 }.freeze
- RETRY_AFTER_CAP =
Retry-After header values larger than this are clamped, matching the reference 2-minute cap.
120.0
Instance Method Summary collapse
-
#initialize(cancel_token: nil) ⇒ BackoffPolicy
constructor
cancel_token: an Interaction::CancelToken (or anything answering #check!) so a backoff wait aborts promptly on Ctrl+C instead of blocking for the full delay.
-
#jittered(attempt, base: ERROR_PATH[:base], max: ERROR_PATH[:max]) ⇒ Object
Jittered delay in seconds for a 1-based attempt.
-
#parse_retry_after(value) ⇒ Object
Pull a Retry-After value from a raw header value (String/Numeric) or a typed error carrying a Faraday response.
-
#sleep(seconds) ⇒ Object
Sleep ‘seconds`, sliced into 100ms ticks, polling the cancel token between ticks so Ctrl+C aborts within ~100ms instead of blocking the whole wait.
-
#wait_seconds(attempt, base:, max:, retry_after: nil) ⇒ Object
The wait to honour for a retry.
Constructor Details
#initialize(cancel_token: nil) ⇒ BackoffPolicy
cancel_token: an Interaction::CancelToken (or anything answering #check!) so a backoff wait aborts promptly on Ctrl+C instead of blocking for the full delay. Optional — nil means a plain (still sliced) sleep.
38 39 40 |
# File 'lib/rubino/agent/backoff_policy.rb', line 38 def initialize(cancel_token: nil) @cancel_token = cancel_token end |
Instance Method Details
#jittered(attempt, base: ERROR_PATH[:base], max: ERROR_PATH[:max]) ⇒ Object
Jittered delay in seconds for a 1-based attempt. ‘base`/`max` default to the error-path preset; pass a preset hash’s values for the other site.
44 45 46 47 48 |
# File 'lib/rubino/agent/backoff_policy.rb', line 44 def jittered(attempt, base: ERROR_PATH[:base], max: ERROR_PATH[:max]) exponent = [0, attempt - 1].max delay = base <= 0 || exponent >= 63 ? max : [base * (2**exponent), max].min delay + (rand * JITTER_RATIO * delay) end |
#parse_retry_after(value) ⇒ Object
Pull a Retry-After value from a raw header value (String/Numeric) or a typed error carrying a Faraday response. Returns Float seconds or nil.
NOTE: only the delta-seconds form (e.g. “30”) is parsed. The HTTP-date form of Retry-After is not handled — no provider this gem targets sends it, and the reference likewise only parses the numeric form. TODO: handle the date form if a provider ever needs it.
81 82 83 84 85 86 87 88 89 90 91 92 93 94 |
# File 'lib/rubino/agent/backoff_policy.rb', line 81 def parse_retry_after(value) return if value.nil? raw = if value.is_a?(Numeric) || value.is_a?(String) value else retry_after_header(value) end return if raw.nil? f = Float(raw, exception: false) f if f&.positive? end |
#sleep(seconds) ⇒ Object
Sleep ‘seconds`, sliced into 100ms ticks, polling the cancel token between ticks so Ctrl+C aborts within ~100ms instead of blocking the whole wait. On cancel, CancelToken#check! raises Interrupted. Mirrors the adapter’s former cancellable_sleep and the reference incremental sleep loop.
66 67 68 69 70 71 72 |
# File 'lib/rubino/agent/backoff_policy.rb', line 66 def sleep(seconds) deadline = monotonic_now + seconds while (remaining = deadline - monotonic_now).positive? @cancel_token&.check! Kernel.sleep([0.1, remaining].min) end end |
#wait_seconds(attempt, base:, max:, retry_after: nil) ⇒ Object
The wait to honour for a retry. When the upstream sent a Retry-After we respect it (clamped to RETRY_AFTER_CAP), exactly as the reference does on the rate-limited path; otherwise fall back to the jittered backoff.
54 55 56 57 58 59 |
# File 'lib/rubino/agent/backoff_policy.rb', line 54 def wait_seconds(attempt, base:, max:, retry_after: nil) ra = parse_retry_after(retry_after) return [ra, RETRY_AFTER_CAP].min if ra jittered(attempt, base: base, max: max) end |