Module: Dinie::Internal::Retry

Defined in:
lib/dinie/runtime/retry.rb

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 ‘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.

Set[408, 429, 500, 502, 503, 504].freeze
INITIAL_BACKOFF_SECONDS =

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

0.5
MAX_BACKOFF_SECONDS =

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

8
JITTER_RATIO =

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

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.

[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

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 (‘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