Module: Dinie::Internal::Retry

Defined in:
lib/dinie/runtime/retry.rb,
sig/dinie/runtime/retry.rbs

Overview

Retry policy — pure decision + delay functions (no I/O, no state). The retry loop itself — sleeping, attempt counting, the X-Dinie-Retry-Count header, the 401 one-shot re-auth — lives in HttpClient (architecture §10, RB15). Mirrors sdk-js retry.ts so the two SDKs share the exact same backoff/jitter/status-set behavior (the comparison.md axis).

Runtime-internal: imported directly by HttpClient, not part of the public surface. The public Retry-After parser lives separately as Dinie.parse_retry_after (story 002).

Constant Summary collapse

RETRYABLE_STATUS =

HTTP status codes the SDK retries (V0.2 freeze decision; architecture §10).

Exactly {408, 429, 500, 502, 503, 504}, plus timeouts/connection errors (handled by retryable_network_error?). 409 (Dinie semantic conflict) and 410 (gone) never retry; 401 is a one-shot re-auth handled in HttpClient, orthogonal to this set. 500 is safe to retry on a non-GET because the stable X-Idempotency-Key (minted once before the loop) guarantees a retry never creates a duplicate resource.

Returns:

  • (Set[Integer])
Set[408, 429, 500, 502, 503, 504].freeze
INITIAL_BACKOFF_SECONDS =

Initial backoff, in seconds (the attempt = 0 base before jitter).

Returns:

  • (Float)
0.5
MAX_BACKOFF_SECONDS =

Backoff ceiling, in seconds (reached at attempt = 4).

Returns:

  • (Integer)
8
JITTER_RATIO =

Subtractive jitter fraction — the delay is reduced by up to this share.

Returns:

  • (Float)
0.25
RETRYABLE_NETWORK_ERRORS =

Faraday transport errors (no HTTP response) worth retrying: a request timeout and a connection failure (DNS/refused/reset). Anything else propagates as a connection error.

Returns:

  • (Array[untyped])
[Faraday::TimeoutError, Faraday::ConnectionFailed].freeze

Class Method Summary collapse

Class Method Details

.retry_delay(attempt, retry_after: nil, retry_after_ms: nil) ⇒ Float

Seconds to wait before the next attempt.

A parseable Retry-After / Retry-After-Ms wins (already clamped to [0, 60] by Dinie.parse_retry_after). Otherwise: exponential backoff min(0.5 · 2^attempt, 8) s minus up to 25% subtractive jitter via rand (assert the band in specs, not the value).

Parameters:

  • attempt (Integer) —

    zero-based attempt index just completed

  • retry_after (String, Array<String>, nil) (defaults to: nil) —

    the response Retry-After header

  • retry_after_ms (String, Array<String>, nil) (defaults to: nil) —

    the response Retry-After-Ms header

  • retry_after: (String, Array[String], nil) (defaults to: nil)
  • retry_after_ms: (String, Array[String], nil) (defaults to: nil)

Returns:

  • (Float) —

    seconds to sleep



65
66
67
68
69
70
71
# File 'lib/dinie/runtime/retry.rb', line 65

def retry_delay(attempt, retry_after: nil, retry_after_ms: nil)
  from_header = Dinie.parse_retry_after(retry_after, retry_after_ms: retry_after_ms)
  return from_header unless from_header.nil?

  base = [INITIAL_BACKOFF_SECONDS * (2**attempt), MAX_BACKOFF_SECONDS].min
  base * (1 - (JITTER_RATIO * rand))
end

.retryable_network_error?(error) ⇒ Boolean

Whether a thrown transport error (no HTTP response) is worth retrying: a timeout or a connection failure. Keys off the Faraday error class, never the message.

Parameters:

  • error (Exception)

Returns:

  • (Boolean)


51
52
53
# File 'lib/dinie/runtime/retry.rb', line 51

def retryable_network_error?(error)
  RETRYABLE_NETWORK_ERRORS.any? { |klass| error.is_a?(klass) }
end

.should_retry?(status) ⇒ Boolean

True only for the retryable status set ({408, 429, 500, 502, 503, 504}).

Parameters:

  • status (Integer)

Returns:

  • (Boolean)


42
43
44
# File 'lib/dinie/runtime/retry.rb', line 42

def should_retry?(status)
  RETRYABLE_STATUS.include?(status)
end