Zum Inhalt springen
Deutsch

Retry

retry ist eine reine Async-Funktion für “versuche diesen Call bis zu N-mal, mit Backoff zwischen den Versuchen.” Es ist die einfachste Schicht im Resilienz-Stack — keine Actors, kein Zustand, nur eine Promise-zurückgebende Factory.

import { retry } from 'actor-ts';
const data = await retry(
() => fetch('https://example.com/items').then(r => r.json()),
{ attempts: 3, delayMs: 200, factor: 2 },
);

Die Factory wird bis zu 3-mal aufgerufen. Jeder Retry wartet länger: Versuch 1 sofort, Versuch 2 nach 200 ms, Versuch 3 nach 400 ms. Gibt den ersten Erfolg zurück; rejected mit dem letzten Fehler, wenn jeder Versuch fehlschlägt.

function retry<T>(factory: () => Promise<T>, options: RetryOptions): Promise<T>;
interface RetryOptions {
attempts: number; // gesamt inklusive initialer Call (>= 1)
delayMs?: number; // Base-Delay zwischen Versuchen
factor?: number; // Multiplikator für Exponential-Backoff (Default 1)
maxDelayMs?: number; // Obergrenze für jedes einzelne Delay
shouldRetry?: (err: Error, attempt: number) => boolean;
onAttempt?: (err: Error, attempt: number) => void;
}

Das attempts-Feld ist Gesamt-Versuche, nicht Retries — attempts: 1 führt die Factory genau einmal ohne Retries aus. attempts: 3 läuft bis zu 3-mal.

Vier Knöpfe kooperieren zur Steuerung des Delays zwischen Versuchen:

SettingWas es tut
delayMsBase-Delay zwischen Versuchen. Default 0 (kein Warten).
factorMultiplikator pro Versuch. Delay bei Versuch N ist delayMs × factor^(N-1). Default 1 (konstant).
maxDelayMsObergrenze für jedes einzelne Delay. Default unbegrenzt.
// Konstant 500ms zwischen Versuchen:
retry(factory, { attempts: 5, delayMs: 500 });
// Exponentiell: 200ms, 400ms, 800ms, gecappt bei 5s:
retry(factory, { attempts: 6, delayMs: 200, factor: 2, maxDelayMs: 5_000 });
// Fibonacci-artig: ad-hoc via factor=1.6:
retry(factory, { attempts: 5, delayMs: 100, factor: 1.6 });

Kein Jitter — das Delay ist deterministisch pro Versuch. Wenn du gejitterte Retries brauchst (empfohlen für High-Concurrency-Szenarien, die synchronisieren könnten), wickle die Policy:

import { retry, exponentialBackoff } from 'actor-ts';
const policy = exponentialBackoff({ minMs: 200, maxMs: 5_000, randomFactor: 0.2 });
async function jitteredRetry<T>(factory: () => Promise<T>, attempts: number): Promise<T> {
for (let i = 0; i < attempts; i++) {
try { return await factory(); }
catch (e) {
if (i === attempts - 1) throw e;
await new Promise(r => setTimeout(r, policy.delayFor(i)));
}
}
throw new Error('unreachable');
}

Oder verwende BackoffSupervisor, wenn das, was retried wird, ein Actor-Restart ist.

class TransientError extends Error {}
class PermanentError extends Error {}
await retry(
() => callExternalAPI(),
{
attempts: 5,
delayMs: 500,
factor: 2,
shouldRetry: (err) => err instanceof TransientError,
},
);

shouldRetry läuft nach jedem fehlgeschlagenen Versuch. Gib false zurück, um kurzzuschließen — die Retry-Loop endet sofort mit diesem Fehler, keine weiteren Versuche.

Verwende es, um zwischen transienten und permanenten Fehlern zu unterscheiden:

  • Transient (Netzwerk-Aussetzer, Rate-Limit, Lock-Contention) → retry.
  • Permanent (Validierungsfehler, Auth-Fehler, 4xx) → nicht retrien; der nächste Versuch wird auf dieselbe Weise fehlschlagen.
await retry(factory, {
attempts: 3,
delayMs: 500,
onAttempt: (err, attempt) => {
metrics.counter('retry.attempt').inc({ attempt });
log.warn(`attempt ${attempt} failed: ${err.message}`);
},
});

Feuert nach jedem fehlgeschlagenen Versuch, inklusive dem letzten. Verwende es für retry-bewusste Metriken und Logging — Counter pro Versuch, Spans fürs Tracing, Alerts auf “uns sind die Retries ausgegangen.”

Use CaseGreife zu…
Ein einzelner Promise-zurückgebender Call (HTTP-Fetch, DB-Query, Ask)retry
Ein Actor, der fehlschlägt und respawned werden sollBackoffSupervisor
Ein Call, geschützt durch KurzschlussverhaltenCircuitBreaker (oft kombiniert mit retry innen)

retry ist das Call-Level-Primitive. BackoffSupervisor ist das Actor-Level-Primitive. Sie konkurrieren nicht — wähle, was du schützt.

Für einen Actor, der seinen eigenen Downstream-Call retrien will, ist retry innerhalb von onReceive okay:

class MyActor extends Actor<...> {
override async onReceive(msg): Promise<void> {
const result = await retry(
() => this.callDownstream(msg),
{ attempts: 3, delayMs: 200, factor: 2 },
);
// ...
}
}

Innerhalb von onReceive zu awaiten, blockiert die Mailbox des Actors, bis der Retry vollendet ist — derselbe Trade-off wie jedes Await innerhalb eines onReceive. Siehe Ask-Pattern dafür, wann das ein Problem ist.

Ein Circuit Breaker ist Per-Dependency-Zustand, der sagt “höre für eine Weile auf, dieses Ding zu versuchen.” Retry ist Per-Call-Logik, die sagt “versuche jetzt nach einem kurzen Warten erneut.”

Kombiniere sie — Retry innerhalb eines Breaker-Calls:

breaker.call(() => retry(
() => fetch('https://flaky.example'),
{ attempts: 3, delayMs: 100, factor: 2 },
));

Der Retry handhabt transiente Aussetzer während eines einzelnen Calls; der Breaker trackt den Trend über viele Calls.

Die retry-API-Referenz deckt die volle Signatur ab.