first commit
This commit is contained in:
93
docs/communication/messaging/sms/src/retry/retry.md
Normal file
93
docs/communication/messaging/sms/src/retry/retry.md
Normal file
@@ -0,0 +1,93 @@
|
||||
# `retry`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`retry` is the exponential-backoff retry policy used by
|
||||
`SmsService.send` to absorb transient provider failures. The
|
||||
policy retries a thrown attempt up to `maxAttempts` times with
|
||||
exponential backoff (`initialDelayMs * 2^attempt`, capped at
|
||||
`maxDelayMs`) and full jitter, awaits each backoff against an
|
||||
injectable `Timers` seam so tests can drive the policy without
|
||||
real wall-clock waits, and honours an `AbortSignal` so cancelled
|
||||
operations don't sit through a multi-second backoff.
|
||||
|
||||
## Features
|
||||
|
||||
- Exponential backoff with full-jitter delay computation.
|
||||
- Injectable `TimersLike` seam — defaults to a
|
||||
`globalThis.setTimeout` adapter; tests inject a controlled
|
||||
timer.
|
||||
- Injectable `random` source — defaults to `Math.random`; tests
|
||||
pin it to a fixed value for deterministic jitter.
|
||||
- AbortSignal-aware — a pre-aborted signal halts before the
|
||||
first attempt; an abort that lands during a backoff cancels
|
||||
the timer and rejects with `SmsAbortedError`.
|
||||
- Per-attempt `onRetry` hook receives a `SmsRetryEvent` so
|
||||
callers can record per-attempt OTel counters / bus events.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { RetryPolicy } from "@suaveplan/sms/retry";
|
||||
|
||||
const policy = new RetryPolicy({ maxAttempts: 3 });
|
||||
const value = await policy.execute(async () => callRemoteService());
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { RetryPolicy } from "@suaveplan/sms/retry";
|
||||
|
||||
const ctrl = new AbortController();
|
||||
const policy = new RetryPolicy({
|
||||
maxAttempts: 5,
|
||||
initialDelayMs: 100,
|
||||
maxDelayMs: 2_000,
|
||||
});
|
||||
|
||||
const result = await policy.execute(
|
||||
async (attempt) => {
|
||||
if (attempt < 2) throw new Error("flaky");
|
||||
return "ok";
|
||||
},
|
||||
{
|
||||
signal: ctrl.signal,
|
||||
onRetry: (event) => {
|
||||
console.log("retry", event.attempt, event.error.message);
|
||||
},
|
||||
to: "+15551234567",
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `class RetryPolicy`
|
||||
- `constructor(options?: RetryPolicyOptions)`
|
||||
- `execute<T>(fn: (attempt: number) => Promise<T>, options?: {
|
||||
signal?: AbortSignal; onRetry?: RetryHook; to?: string;
|
||||
}): Promise<T>`
|
||||
- `type RetryHook = (event: SmsRetryEvent) => void`.
|
||||
- `interface RetryPolicyOptions`
|
||||
- `maxAttempts?: number` (default 3)
|
||||
- `initialDelayMs?: number` (default 50)
|
||||
- `maxDelayMs?: number` (default 5000)
|
||||
- `timers?: TimersLike`
|
||||
- `random?: () => number`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The backoff uses full jitter (`floor(random() * capped)`) which
|
||||
empirically produces lower contention than equal jitter for SMS
|
||||
provider rate-limit scenarios. The `onRetry` hook receives the
|
||||
ZERO-based attempt index for the attempt that just failed, so
|
||||
the first retry fires `attempt = 0`. The delay loop guards
|
||||
against abort via an `AbortSignal.addEventListener("abort", …,
|
||||
{ once: true })` listener that clears the pending timer and
|
||||
rejects synchronously, ensuring an aborted retry never wakes up
|
||||
to perform the next attempt.
|
||||
Reference in New Issue
Block a user