Zum Inhalt springen
Deutsch

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.

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); // ~400ms
policy.delayFor(2); // ~800ms
policy.delayFor(3); // ~1600ms
policy.delayFor(4); // ~3200ms
policy.delayFor(5); // ~6400ms
policy.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:

FeldBedeutung
minMsUntergrenze für das Delay. Der erste Retry wartet mindestens so lang.
maxMsObergrenze 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.

import { linearBackoff } from 'actor-ts';
const policy = linearBackoff({
minMs: 500,
maxMs: 5_000,
stepMs: 500,
});
policy.delayFor(0); // ~500ms
policy.delayFor(1); // ~1000ms
policy.delayFor(2); // ~1500ms
policy.delayFor(3); // ~2000ms
policy.delayFor(8); // ~4500ms
policy.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.

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.

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.

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.

  • Backoff-Supervisor — der Actor-Restart-Wrapper, der eine BackoffPolicy konsumiert.
  • 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.