Backoff-Policy
Eine Backoff-Policy ist eine Funktion von “wie oft ist das fehlgeschlagen?” zu “wie lange soll ich warten, bevor ich es erneut versuche?” Entkoppelt von jedem spezifischen Retry oder Supervisor, sodass die Berechnung trivial testbar und wiederverwendbar ist.
interface BackoffPolicy { delayFor(restartCount: number): number;}restartCount ist 0-basiert — der erste Retry übergibt 0, der
zweite 1 und so weiter. Gibt das Delay in Millisekunden zurück.
Zwei Eingebaute, beide produzieren Werte, die du in einen
BackoffSupervisor stecken oder
standalone verwenden kannst.
Exponential Backoff
Abschnitt betitelt „Exponential Backoff“import { exponentialBackoff } from 'actor-ts';
const policy = exponentialBackoff({ minMs: 200, maxMs: 10_000, randomFactor: 0.2,});
policy.delayFor(0); // ~200ms (±20% Jitter)policy.delayFor(1); // ~400mspolicy.delayFor(2); // ~800mspolicy.delayFor(3); // ~1600mspolicy.delayFor(4); // ~3200mspolicy.delayFor(5); // ~6400mspolicy.delayFor(6); // ~10_000 (gecappt)policy.delayFor(30); // ~10_000 (immer noch gecappt)Die Formel: min × 2^n gecappt auf max, multipliziert mit
1 + random(-randomFactor, +randomFactor).
Drei Knöpfe:
| Feld | Bedeutung |
|---|---|
minMs | Untergrenze für das Delay. Der erste Retry wartet mindestens so lang. |
maxMs | Obergrenze für das Delay. Sobald min × 2^n das überschreitet, gibt die Policy maxMs zurück (gejittert). |
randomFactor (Default 0.2) | Jitter-Anteil in [0, 1]. Das tatsächliche Delay ist base * (1 + sign * randomFactor), wobei sign gleichverteilt in [-1, 1] ist. |
Warum exponentiell? Ein fehlschlagendes Upstream braucht meist
Zeit zum Erholen. Das Warten zu verdoppeln, gibt ihm mehr Zeit
ohne unendliche Delays. Und das Cappen bei max verhindert
pathologisch lange Wartezeiten nach vielen Fehlern.
Warum Jitter? Wenn N Clients alle mit dem gleichen Delay retrien, synchronisieren sie ihren nächsten Versuch — der nächste Retry ist eine donnernde Herde, was die Chance erhöht, dass das Upstream kaputt bleibt. ±20 % Rauschen hinzuzufügen, streut die Retries in der Zeit, sodass die Herde sich auflöst.
Linear Backoff
Abschnitt betitelt „Linear Backoff“import { linearBackoff } from 'actor-ts';
const policy = linearBackoff({ minMs: 500, maxMs: 5_000, stepMs: 500,});
policy.delayFor(0); // ~500mspolicy.delayFor(1); // ~1000mspolicy.delayFor(2); // ~1500mspolicy.delayFor(3); // ~2000mspolicy.delayFor(8); // ~4500mspolicy.delayFor(9); // ~5000ms (gecappt)policy.delayFor(20); // ~5000ms (immer noch gecappt)Die Formel: min + step × n gecappt auf max, mit demselben
Jitter-Vertrag.
Wann linear? Nische — wenn du spezifisch begrenztes Wachstum statt exponentiell willst. Beispiele:
- Eine Poll-Kadenz, die schnell abflachen soll (alle 500 ms, 1 s, 1,5 s, … gecappt bei 5 s).
- Ein Producer, der sich selbst rate-limited, wenn er Backpressure empfängt — die Wachstumsrate der Wartezeit matcht die steady-state-akzeptable Last auf dem Consumer.
Exponentiell ist meist die bessere Wahl für Recovery (gib dem Upstream Zeit zu heilen). Linear ist besser für Steady-State-Adaption.
Determinismus in Tests
Abschnitt betitelt „Determinismus in Tests“Beide Policies akzeptieren ein optionales random: () => number-Override:
let seed = 0;const deterministicRandom = () => (seed = (seed * 9301 + 49297) % 233280) / 233280;
const policy = exponentialBackoff({ minMs: 100, maxMs: 10_000, randomFactor: 0.2, random: deterministicRandom,});
// Jetzt ist `policy.delayFor(n)` reproduzierbar.Ein linearer Kongruenzgenerator ist für Tests gut genug; für
Produktions-Zufälligkeit ist das Default-Math.random in Ordnung.
Standalone-Nutzung
Abschnitt betitelt „Standalone-Nutzung“Die Policy muss nicht in einen Supervisor fließen. Überall, wo du
await new Promise(r => setTimeout(r, computeDelay(n))) schreiben
würdest, stecke eine Policy ein:
import { exponentialBackoff } from 'actor-ts';
const policy = exponentialBackoff({ minMs: 200, maxMs: 5_000 });
let attempt = 0;while (true) { try { const result = await someRiskyCall(); return result; } catch (err) { const delay = policy.delayFor(attempt++); await new Promise(r => setTimeout(r, delay)); if (attempt >= 5) throw err; }}Für den häufigeren Fall “N-mal mit Backoff retrien” siehe
Retry, das eigene Delay-Optionen hat (ohne
durch BackoffPolicy zu gehen). Die Standalone-Policy ist am
nützlichsten, wenn du mit einer bestehenden
Retry/Scheduler-Abstraktion integrierst und nur das
Delay-Berechnungs-Stück brauchst.
Eigene Policies
Abschnitt betitelt „Eigene Policies“Implementiere das BackoffPolicy-Interface:
import type { BackoffPolicy } from 'actor-ts';
// Fibonacci-Backoff — zum Spaß.function fibBackoff(opts: { maxMs: number }): BackoffPolicy { return { delayFor(n: number): number { let a = 100, b = 100; for (let i = 0; i < n; i++) { [a, b] = [b, a + b]; } return Math.min(a, opts.maxMs); }, };}Übergib es an BackoffSupervisor via die policy-Option, und du
hast das Default-Exponential-Wachstum überschrieben.
Wie es weitergeht
Abschnitt betitelt „Wie es weitergeht“- Backoff-Supervisor —
der Actor-Restart-Wrapper, der eine
BackoffPolicykonsumiert. - Retry — Per-Call-Retry mit
eigenen Delay-Optionen; geht nicht durch
BackoffPolicy, aber die zugrunde liegende Mathematik ist ähnlich. - Circuit Breaker — für wenn “länger zwischen Versuchen warten” zu “eine Weile gar nicht mehr versuchen” eskalieren soll.
Die exponentialBackoff- und
linearBackoff-API-Referenzen
decken die vollen Optionen ab.