RetryPolicy
in package
Bounded read-only retry policy — CONTRACT.md §16.
This SDK had no §16 policy before D5 — only §9's single-flight refresh
coordination, which is a different mechanism (OidcClient's
for ($attempt = 0; $attempt < 3; $attempt++) loop coordinates concurrent
refreshes; it does not retry a transport failure). §11.2 rule 5 and §14.2
rule 6 had both been requiring "the SDK's existing bounded read-only retry
policy" against a policy that did not exist here.
Table of Contents
Constants
- BASE_DELAY_MS : mixed = 200.0
- First backoff step, in milliseconds (§16.1).
- MAX_ATTEMPTS : mixed = 3
- Attempt cap: 1 initial + 2 retries (§16.1).
- MAX_DELAY_MS : mixed = 5000.0
- Ceiling on any single computed backoff, in milliseconds (§16.1).
Methods
- backoffMs() : float
- The un-jittered backoff for a 1-based attempt: `min(MAX_DELAY_MS, BASE_DELAY_MS * 2^(n-1))`.
- delayMs() : float
- The actual wait: **full jitter** over `[0, backoff]`, raised to any server-supplied `Retry-After` (§16.1).
- execute() : T
- Runs `$operation` under the §16 policy.
Constants
BASE_DELAY_MS
First backoff step, in milliseconds (§16.1).
public
mixed
BASE_DELAY_MS
= 200.0
MAX_ATTEMPTS
Attempt cap: 1 initial + 2 retries (§16.1).
public
mixed
MAX_ATTEMPTS
= 3
MAX_DELAY_MS
Ceiling on any single computed backoff, in milliseconds (§16.1).
public
mixed
MAX_DELAY_MS
= 5000.0
Methods
backoffMs()
The un-jittered backoff for a 1-based attempt: `min(MAX_DELAY_MS, BASE_DELAY_MS * 2^(n-1))`.
public
static backoffMs(int $attempt) : float
Attempt 1 → 200ms, attempt 2 → 400ms.
Parameters
- $attempt : int
-
The 1-based attempt number.
Return values
float —The backoff in milliseconds.
delayMs()
The actual wait: **full jitter** over `[0, backoff]`, raised to any server-supplied `Retry-After` (§16.1).
public
static delayMs(int $attempt, float $retryAfterMs, float $fraction) : float
Full jitter, not backoff ± 10%. Partial jitter keeps every client's
retries clustered around the same instant, which is the thundering herd
retries are supposed to prevent rather than cause.
Retry-After is a floor, never a ceiling: the server is stating when
it will be ready, so retrying sooner is not permitted — and a
Retry-After: 0 cannot shorten the wait below what jitter chose.
Parameters
- $attempt : int
-
The 1-based attempt that just failed.
- $retryAfterMs : float
-
A server-supplied hint in milliseconds, or 0.0.
- $fraction : float
-
The jitter draw in
[0, 1], injected so tests can pin it.
Return values
float —The wait in milliseconds.
execute()
Runs `$operation` under the §16 policy.
public
static execute(string $operationName, bool $enabled, TelemetryDispatcher $telemetry, callable(int): T $operation[, callable(): float|null $jitter = null ][, callable(float): void|null $sleep = null ][, callable(NetworkError): bool|null $retryable = null ]) : T
$operation receives the 1-based attempt number so it can label its §19
request pair — §19.2 rule 5 requires one pair per attempt so a caller can
count real wire calls, and passing 1 every time would make a retried call
indistinguishable from a single slow one.
$operation MUST be side-effect-free. This helper — like every retry
helper — cannot tell the difference, so routing a mutation through it
would silently duplicate a side effect, or replay a single-use credential
(an authorization code, a device code at redemption, a rotating refresh
token) into a hard invalid_grant.
Only NetworkError is retried. The §2 taxonomy folds
408/429/5xx/transport into that one type, so this implements the
whole §16.3 table: AuthError and AuthzError are decisive answers from
the server, not transport failures.
Parameters
- $operationName : string
-
Canonical name, for the §19 event.
- $enabled : bool
-
§16.1 disable switch.
- $telemetry : TelemetryDispatcher
-
Notified before each retry wait.
- $operation : callable(int): T
-
The side-effect-free operation.
- $jitter : callable(): float|null = null
-
Injected jitter draw, for tests.
- $sleep : callable(float): void|null = null
-
Injected sleep, for tests.
- $retryable : callable(NetworkError): bool|null = null
-
Decides whether a caught NetworkError is eligible at all;
nullkeeps the previous behaviour of retrying every one. CONTRACT.md §27 needs this for two reasons: §27.4 rule 8 retries onlyGET, and §27.4 rule 7 puts ValidationError UNDERNetworkError, so a body the server has already rejected would otherwise be sent three times.