Zum Inhalt springen
Deutsch

Mailbox-Sizing

Die Default-Mailbox eines Actors ist unbounded. Auf dem Weg hinein wird nichts verworfen, und die Queue wächst, bis der Actor sie abarbeitet. Eine zu begrenzen ist eine Entscheidung pro Actor, denn es ist eine Entscheidung, Nachrichten zu verlieren — du triffst sie dort, wo du sagen kannst, welche verzichtbar sind.

Diese Seite ist der Entscheidungs-Guide für Produktions-Mailbox-Sizing.

const ref = system.spawnAnonymous(Worker);
// ↑ unbounded FIFO-Mailbox — auf dem Weg hinein wird nie etwas verworfen

Unbounded ist der ehrliche Default: ein Actor-Framework kann nicht wissen, welche deiner Nachrichten entbehrlich ist — und zwischen v0.10 und v0.15 hat dieses geraten. Es begrenzte jede Mailbox auf 10 000 mit drop-head, ein zurückfallender Actor verlor also still seine älteste wartende Nachricht. Für einen Sensorwert ist das richtig, für ein Terminated-Signal, eine Delivery-Bestätigung oder ein WebSocket-close falsch. Alle drei liefen durch dieselbe Queue, und aus jedem wurde ein gemeldeter Defekt.

Die Obergrenze, die dieser Handel erkaufte, gab es ohnehin nicht: nur die User-Queue war begrenzt. System-Nachrichten waren nie gedeckelt, der Prozess konnte den Heap also trotzdem ausschöpfen.

Was du stattdessen bekommst, ist Wachstum, von dem du erfährst:

  • Ein Actor, dessen Queue 10 000 Nachrichten erreicht, loggt eine Warnung — und erneut bei jeder Verdopplung: 20 000, 40 000 und so weiter. Das läuft immer, ohne Metrik-Stack.
  • Mit aktivierten Metriken meldet actor_mailbox_size{class, path} die Tiefe jeder Mailbox ab derselben Marke.

Der Fehlermodus, den eine unbegrenzte Queue weiterhin erreichen kann — erschöpfter Heap, lange GC-Pausen, ein Producer, der nie erfährt, dass es ein Problem gibt — kündigt sich also lange vorher an. Achte auf die Warnung; begrenze die Actors, die sie produzieren.

Drei Muster, in denen eine unbegrenzte Queue die falsche Antwort ist. In jedem sind Kapazität und Policy bewusste Entscheidungen:

import { ActorOptions } from 'actor-ts';
// Langsamer Consumer: schreibt 10/sec auf Disk; Producer pusht 1000/sec
const writerOptions = ActorOptions.create()
.withMailbox(() => new BoundedMailbox({
capacity: 1_000,
overflow: 'reject',
}));
const slowWriter = system.spawnAnonymous(SlowWriter, writerOptions);

Bounde auf den Worst-Case-akzeptablen Puffer. reject propagiert Backpressure zum Sender — er sieht MailboxFullError und passt sich an (Retry, Drop, Alert).

2. Telemetrie-artige Actors (veraltete Daten sind falsch)

Abschnitt betitelt „2. Telemetrie-artige Actors (veraltete Daten sind falsch)“
import { ActorOptions } from 'actor-ts';
const telemetryOptions = ActorOptions.create()
.withMailbox(() => new BoundedMailbox({
capacity: 5_000,
overflow: 'drop-head',
}));
const telemetry = system.spawnAnonymous(MetricsAggregator, telemetryOptions);

Für Metriken, Sensorwerte, Status-Pings — frischer ist besser. drop-head verwirft die älteste wartende Nachricht, wenn neue ankommen, und hält die Queue mit aktuellen Daten gefüllt.

import { ActorOptions } from 'actor-ts';
const authOptions = ActorOptions.create()
.withMailbox(() => new BoundedMailbox({
capacity: 10_000,
overflow: 'drop-new',
}));
const auth = system.spawnAnonymous(AuthActor, authOptions);

drop-new verwirft eingehende Nachrichten, wenn voll — bewahrt bereits eingereihte Arbeit. Richtig, wenn “die Queue, die ich habe, ist die Arbeit, die mich interessiert” gilt — teilweiser Denial of Service ist besser, als gar nichts zu verarbeiten.

Drei Faktoren:

  1. Worst-Case-Burst-Größe — wieviele Nachrichten im Worst-Case-Fenster ankommen, bevor der Consumer drainen kann.
  2. Speicher pro Nachrichtcapacity × bytes_per_message begrenzt die Speicherkosten.
  3. Latenz-Budgetcapacity / drain_rate begrenzt die Worst-Case-Latenz, die eine Nachricht vor der Verarbeitung wartet.

Für einen Worker, der 100 msg/sec verarbeitet und Bursts bis zu 1000 msg in 1 Sekunde erwartet:

capacity = 1000 # Worst-Case-Burst
Worst-Case-Latenz = 1000 / 100 = 10s # bei voller Queue

Wenn 10 Sekunden Queue okay sind, ist Kapazität 1000 fein. Wenn nicht, Kapazität reduzieren oder akzeptieren, dass Producer MailboxFullError sehen.

Stock-Metriken (Stock-Metriken) exponieren die Mailbox-Tiefe:

actor_mailbox_size{class="Worker", path="..."}
actor_mailbox_dropped_total{class="Worker", path="...", reason="drop-head"}

Beobachte:

  • actor_mailbox_size — die Tiefe jeder Mailbox ab 10 000 wartenden Nachrichten. Eine Serie entsteht erst, wenn ein Actor diese Marke überschreitet — ihr Vorhandensein ist also das Signal; auf einem gesunden System ist die Metrik leer. Eine geleerte Mailbox liest 0 statt ihres letzten Ausschlags.
  • actor_mailbox_dropped_total — ungleich Null mit drop-head / drop-new ist bei den Actors beabsichtigt, die du begrenzt hast; bei jedem anderen sollte sie gar nicht auftauchen.
  • MailboxFullError-Rate beim Sender — taucht meist als Supervisor-Restarts des sendenden Actors auf.

Dieselbe 10 000er-Schwelle erzeugt eine Log-Warnung, wiederholt bei jeder Verdopplung, unabhängig davon ob Metriken aktiv sind. Auf diese Zeile alarmierst du, wenn du keinen Metrik-Stack betreibst.

PolicyWann
rejectBackpressure schlägt zum Sender durch. Der Sender muss handeln.
drop-headTelemetrie / Metriken — Neueste gewinnt.
drop-newKritische Arbeit — Eingereihtes behalten, Eingehendes droppen.

Wähle nach der richtigen Antwort auf Overflow:

  • “Sender soll retryen / alerten” → reject.
  • “Veraltete Daten sind falsch” → drop-head.
  • “Eingereihte Arbeit ist wertvoll” → drop-new.

Es gibt kein “Bestes” — kontextabhängig.

Für Actors mit gemischter Dringlichkeit:

import { ActorOptions, PriorityMailbox } from 'actor-ts';
const workerOptions = ActorOptions.create<Message>()
.withMailbox(() => new PriorityMailbox<Message>({
priorityFor: (m) => m.kind === 'urgent' ? 0 : 5,
}));
const worker = system.spawnAnonymous(Worker, workerOptions);

Niedrigere Zahlen = höhere Priorität. System-Nachrichten übertrumpfen immer.

Nutze für:

  • HTTP-Antworten (dringend) vs Batch-Jobs (aufschiebbar).
  • Health-Pings vs Bulk-Metriken.

Siehe Mailboxes für die volle PriorityMailbox-Oberfläche.

producer → reject Backpressure → Sender verlangsamt sich
producer → drop-head → Producer macht weiter; Reader sieht das Neueste
producer → drop-new → Producer macht weiter; Reader verarbeitet das Älteste

Bounded Mailboxes sind eine Ebene in einer Backpressure-Story. Für Ende-zu-Ende-Backpressure (das vorgelagerte System wird langsamer) kombinierst du:

  • Bounded Mailbox am Actor.
  • Sender-Retry-Logik.
  • Upstream-Rate-Limiting (HTTP 429, Broker-Pushback).

Die Mailbox erzwingt die lokale Grenze; der Rest ist dein Protokoll-Design.