Singleton — Überblick
Ein Cluster-Singleton ist ein Actor, der genau einmal im gesamten Cluster existiert. Er läuft auf dem Leader-Node; wenn dieser Node geht, spawnt der nächst-gewählte Leader ihn neu. Aufrufer auf jedem Node schicken Nachrichten über einen Proxy, der dorthin routet, wo der Singleton aktuell lebt.
Drei Actors pro Node lassen das funktionieren:
- ClusterSingletonManager — auf jedem Node. Beobachtet Cluster-Events; spawnt den Singleton, wenn dieser Node Leader wird, und stoppt ihn, wenn er aufhört, Leader zu sein.
- ClusterSingletonProxy — auf jedem Node, der mit dem Singleton
spricht. Genau das geben
startundrefzurück: einen weiterleitendenActorRef, der immer auf den Manager des aktuellen Leaders zeigt. - Der Singleton-Actor selbst — der Actor des Nutzers, immer nur auf dem Leader instanziiert.
Ein Node, der nur einen Proxy betreibt, kann das Singleton adressieren, es aber nie hosten — siehe Einen Ref bekommen, ohne zu hosten.
Wann einen Singleton verwenden
Abschnitt betitelt „Wann einen Singleton verwenden“Die klassischen Anwendungsfälle:
- Ein Koordinator — ein Job-Scheduler, ein Saga-Orchestrator, ein Rate-Limit-Budget-Tracker — der eine konsistente Sicht für den ganzen Cluster produzieren muss.
- Ein Owner einer externen Ressource — der Actor, der eine Verbindung zu einem einzelnen externen System hält (ein Lizenzserver, eine Legacy-DB mit Einzelverbindungslizenz).
- Ein Leader-gewählter Service — deine eigene gewählte Rolle für eine clusterweite Verantwortung.
Wenn du if (!alreadyExists) spawn(...) schreiben würdest, ist
ein Singleton vermutlich das Richtige.
Ein minimales Beispiel
Abschnitt betitelt „Ein minimales Beispiel“import { Actor, ActorSystem, Cluster, ClusterOptions, SingletonKey } from 'actor-ts';
class JobScheduler extends Actor<JobCommand> { static readonly singleton = SingletonKey.of<JobCommand>('job-scheduler'); override onReceive(message: JobCommand): void { /* ... */ }}
const system = ActorSystem.create('my-app');const clusterOptions = ClusterOptions.create() .withHost(host) .withPort(port) .withSeeds(seeds);const cluster = await Cluster.join(system, clusterOptions);
// Auf jedem Node: ein Aufruf spawnt den Manager dieses Nodes und gibt den// Ref zurück. Nur der Manager des Leaders konstruiert tatsächlich den// JobScheduler.const scheduler = cluster.singleton.start(JobScheduler);
// Irgendwo in der App — derselbe Aufruf auf jedem Node:scheduler.tell({ kind: 'schedule', jobId: '42' });start gibt einen normalen ActorRef<JobCommand> zurück — ein
Singleton wird also herumgereicht und gespeichert wie jeder andere
Actor, und keine Consumer-Signatur muss wissen, dass es eines ist.
Hinter den Kulissen findet der Ref den Manager des aktuellen Leaders
über den bekannten Pfad /system/cluster/singleton/manager-<typeName>
und leitet Nachrichten dorthin. Bei Leader-Wechseln verschiebt sich
das Ziel automatisch innerhalb einer Gossip-Runde.
Den Key auf der Actor-Klasse deklarieren
Abschnitt betitelt „Den Key auf der Actor-Klasse deklarieren“SingletonKey.of<Command>('type-name') verbindet den Namen eines
Singletons mit seinem Message-Typ. Deklariert als
static readonly singleton auf dem Actor wird beides am Aufrufort
nicht wiederholt, und start / ref leiten den richtigen
ActorRef<Command> allein aus der Klasse ab.
Ein Actor mit Constructor-Dependencies bekommt eine Factory als zweites Argument:
class UserRepository extends PersistentActor<UserRepositoryCommand, Event, State> { static readonly singleton = SingletonKey.of<UserRepositoryCommand>('user-repository'); constructor(private readonly users: ActorRef<UserCommand>) { super(); }}
const users = cluster.sharding.start(UserActor);const repository = cluster.singleton.start(UserRepository, () => new UserRepository(users));Eine Rolle gehört auf den Key, als zweites Argument:
class Ingress extends Actor<IngressCommand> { static readonly singleton = SingletonKey.of<IngressCommand>('ingress', 'edge');}Sie ist nicht Teil der Identität — zwei Keys sind genau dann gleich,
wenn ihre Namen übereinstimmen — sie fährt nur mit, damit jede Node
dieselbe liest. Das zählt für eine Node, die nur ref aufruft: sie
hat kein Options-Objekt, eine allein über withRole gesetzte Rolle ist
für sie also unsichtbar, und ihr Proxy würde einen anderen Host
auflösen als die Manager.
Für lease, oder um die Rolle pro Deployment zu überschreiben, werden
Options danebengelegt — oder die volle Builder-Form genutzt, wenn es
keine Klasse gibt, an die ein Static passt:
const singletonOptions = StartSingletonOptions.create<JobCommand>() .withRole('control-plane') // schlägt eine auf dem Key deklarierte Rolle .withBufferSize(5_000); // Nachrichten, während keine Node hostetcluster.singleton.start(JobScheduler, singletonOptions);bufferSize begrenzt, was der Proxy hält, solange der Cluster keinen
Host hat — normalerweise eine Gossip-Runde, in einem Ausfall aber
unbegrenzt. Jenseits des Limits (Default 1000) gehen Nachrichten mit
einer Warnung in die Dead Letters, statt den Puffer ewig wachsen zu
lassen.
Einen Ref bekommen, ohne zu hosten
Abschnitt betitelt „Einen Ref bekommen, ohne zu hosten“start nimmt den Node in die Rotation auf: er kann Host werden. Ein
Node, der nur mit dem Singleton sprechen muss, ruft stattdessen
ref — das Gegenstück zu ClusterSharding.startProxy:
// Kein Manager auf diesem Node — Nachrichten gehen an den, der hostet.const scheduler = cluster.singleton.ref(JobScheduler);scheduler.tell({ kind: 'schedule', jobId: '42' });ref und start geben für denselben Key denselben memoisierten Ref
zurück, und der lokale Manager wird pro Zustellung aufgelöst — ein Node,
der erst ref und später start ruft, behält also denselben Ref, der
dann einfach lokal statt über die Wire zustellt.
Zwei Dinge tut der zurückgegebene Ref außerdem nicht:
ref.stop()ist ein warnender No-Op. Überall sonst schicktActorRef.stop()einePoisonPillan sein Ziel; hier würde das töten, was der aktuelle Leader gerade hostet. Um diesen Node aus der Rotation zu nehmen, rufecluster.singleton.stop(key).- Stoppen ist asynchron. Der Manager gibt Lease und Envelope-Pfad
in
postStopfrei, und der Actor-Name bleibt belegt, bis die Terminierung abgeschlossen ist — dasselbe Singleton im selben Turn erneut zu starten wirft daher mit einer Erklärung statt mit einem Duplicate-Name-Fehler.
cluster.singleton bietet zusätzlich isStarted(key) und
managerFor(key) für Diagnose.
Failover
Abschnitt betitelt „Failover“Wenn der Host-Node den Cluster verlässt:
- Erkennung: Cluster-Gossip propagiert
MemberLeft/MemberRemovedfür den gehenden Node. - Wahl: der Cluster wählt einen neuen Leader (deterministisch basierend auf der Mitglieder-Sortierreihenfolge).
- Spawn: der Manager des neuen Leaders spawnt den Singleton.
- Routing-Verschiebung: Proxies auf jedem Node sehen den Leader-Wechsel und aktualisieren ihr Forwarding-Ziel.
In-Flight-Nachrichten während des Übergangs landen in Dead Letters, es sei denn, du hast Durability konfiguriert — siehe “State über Failover” weiter unten.
Das Übergangsfenster wird vom Timeout des Failure Detectors begrenzt (typisch ein paar Sekunden für Unreachable-Erkennung). Singletons sind kein Low-Latency-Failover-Werkzeug; sie tauschen etwas Unverfügbarkeit beim Failover gegen die starke Invariante “genau eine Instanz”.
State über Failover
Abschnitt betitelt „State über Failover“Die neue Instanz startet mit weißer Weste — so wie ein neu gestarteter Actor auf einem einzelnen Node. Für State, der überleben soll:
PersistentActor— der Singleton persistiert Events; die neue Instanz spielt sie aus dem Journal ab. Die meisten Produktions-Singletons nutzen das. Siehe PersistentActor.DurableState— einfacher: snapshotte den aktuellen State; restore beim Neustart. Siehe DurableState.DistributedData— für State, der vor dem Neustart des Singletons lesbar sein soll. Die meisten Singletons brauchen das nicht; ihr State ist privat zum Singleton.
Ohne eine dieser Lösungen ist jedes Failover ein Frischstart. Für einen kurzlebigen Koordinator, der nur eingehende Arbeit routet, ist das oft in Ordnung; für stateful Workflows persistiere.
Split-Brain und die optionale Lease
Abschnitt betitelt „Split-Brain und die optionale Lease“Wenn der Cluster partitioniert wird, könnten zwei Hälften jeweils einen eigenen Leader wählen — und beide würden den Singleton spawnen. Genau der Fall, gegen den Singletons existieren.
Drei Verteidigungen, in der Reihenfolge der Komplexität:
- Eine Downing-Strategie, die während einer Partition eine Gewinnerseite wählt (Standardoption, keine Lease nötig). Die Verliererseite fährt sich selbst herunter; nur eine Hälfte bleibt aktiv. Siehe Downing-Strategien.
- Eine Lease, die mit den Start-Options übergeben wird:
Der Manager muss die Lease erfolgreich erwerben, bevor er den Singleton spawnen darf. Nur eine Seite einer Partition kann die Lease halten, sodass selbst bei zwei Leadern nur ein Singleton existiert. Siehe Singleton mit Lease.const singletonOptions = StartSingletonOptions.create<JobCommand>().withLease(someLeaseImpl); // z. B. K8s-Lease oder In-Memory für Testscluster.singleton.start(JobScheduler, singletonOptions);
Die Kombination aus “Downing-Strategie + Lease” ist paranoid-sicher; jeder allein reicht meist.
Ein Singleton hat Overhead jenseits eines normalen Actors:
- Jeder Node fährt einen Manager — sie sind leichtgewichtig (eine Zustandsmaschine, die Cluster-Events beobachtet), aber sie existieren auf jedem Node.
- Jeder Node, der den Singleton aufruft, fährt einen Proxy —
ebenfalls leichtgewichtig, aber pro
tellein zusätzlicher Hop. - Leader-Wechsel ist die Kosten des Failovers — ein Singleton ist die paar Sekunden lang unverfügbar, die der Cluster für die Konvergenz auf einen neuen Leader braucht.
Wenn Exaktheit nicht erforderlich ist (dir würden N Replicas genügen), nutze einen Cluster-Router oder Sharding — beide skalieren horizontal ohne Leader-Flaschenhals.
Wann NICHT verwenden
Abschnitt betitelt „Wann NICHT verwenden“Wohin als Nächstes
Abschnitt betitelt „Wohin als Nächstes“- ClusterSingletonManager — der Per-Node-Manager, der den Singleton wählt + spawnt.
- Singleton mit Lease — Split-Brain-Schutz über eine Koordinations-Lease.
- Coordination — die Lease-Abstraktion selbst.
- Sharding-Überblick — für das Per-Key-Actor-Muster.
- Cluster-Überblick — die Mitgliedschafts- und Leader-Wahl-Maschinerie darunter.
Die ClusterSingletonManager-
und ClusterSingletonProxy-
API-Referenzen decken die vollständige Konfigurationsoberfläche
ab.
