Zum Inhalt springen
Deutsch

Singleton mit Lease

Der Singleton-Manager wählt standardmäßig einen Singleton allein basierend auf Cluster-Gossip. Bei Partitionen + unzureichendem Downing können beide Hälften ihren eigenen Leader wählen → zwei Singletons existieren.

Die Single-Writer-Lease verhindert das:

import { StartSingletonOptions } from 'actor-ts/cluster';
import { KubernetesLease, KubernetesLeaseOptions } from 'actor-ts/coordination';
const kubernetesLeaseOptions = KubernetesLeaseOptions.create()
.withName('job-scheduler-singleton')
.withOwner(process.env.POD_NAME!)
.withTtlMs(30_000)
.withNamespace(process.env.K8S_NAMESPACE!);
const startSingletonOptions = StartSingletonOptions.create()
.withTypeName('job-scheduler')
.withActor(JobScheduler)
.withLease(new KubernetesLease(
kubernetesLeaseOptions,
));
cluster.singleton.start(startSingletonOptions);

Jetzt spawnt der Manager den Singleton erst nach dem Erwerb der Lease. Zwei Manager, die gleichzeitig Leadership beanspruchen, liefern sich ein Rennen um die Lease; nur einer gewinnt.

Lease-Backendmanager_Bmanager_ALease-Backendmanager_Bmanager_ANetzwerkpartition —sowohl A als auch B sehen sich als LeaderRennen — atomares CASPartition heilt — Gossip konvergiertLease erwerbenLease erwerbenErfolgFehlerspawne Singletonbleibe passivonLost feuert auf der zurückgetretenen Seite

Das Lease-Backend (K8s-API-Server) liefert die atomare Exactly-One-Holder-Garantie — jenseits der eventuellen Konsistenz von Gossip.

const startSingletonOptions = StartSingletonOptions.create()
.withTypeName(typeName)
.withActor(singletonActor)
.withLease(lease) // ← optionale Lease
.withAcquireRetryIntervalMs(5_000); // Default — Retry nach fehlgeschlagenem Acquire
cluster.singleton.start(startSingletonOptions);

Lease ist dieselbe Abstraktion wie bei Coordination — InMemoryLease für Tests, KubernetesLease für Produktion.

SetupSingleton-Einzigartigkeitsgarantie
Kein Downing, keine LeaseHält, solange der abtretende Host den Hand-over beantwortet. Eine Partition läuft in den Timeout, danach hosten beide Hälften.
Nur Downing-StrategieDasselbe, aber die Partition wird innerhalb von downAfterMs aufgelöst statt anzuhalten.
Downing + LeaseDas Stärkste, was hier verfügbar ist: Arbitrierung durch eine dritte Instanz, die beide Seiten erreichen können, und die Lease wird erst freigegeben, wenn die abtretende Instanz terminiert ist.

Für Singletons, bei denen Doppelausführung echten Schaden anrichten würde (Kunden doppelt belasten, Events doppelt veröffentlichen), nutze beides.

Keine dieser Zeilen sagt „unmöglich“, und das ist Absicht. Ohne Lease macht der Hand-over „höchstens eine“ wahr, solange der Amtsinhaber erreichbar ist und antwortet — und einen Peer, der nicht antworten kann, kann man auch nicht bitten zurückzutreten. Der eintretende Host nimmt dann handOverTimeoutMs in Kauf und hostet trotzdem, mit einer Warnung, dass die Invariante nicht bewiesen wurde. Mit einer Lease liegt der Schiedsrichter außerhalb beider Hälften, und genau das ändert die Antwort; was bleibt, ist die Garantie des Lease-Providers selbst — der nächste Abschnitt beschreibt, wie ein Ausfall davon sich verhält.

// Innerhalb des Managers (vom Framework verwaltet):
lease.onLost((reason) => {
// Stoppe den Singleton; retry acquire nach acquireRetryIntervalMs
});

Wenn die Lease widerrufen wird (TTL abgelaufen, anderer Halter hat sie übernommen):

  • Der Manager stoppt den Singleton mit einem PoisonPill — die Instanz arbeitet also zuerst ab, was schon in ihrer Mailbox liegt. Das ist ein geordnetes Drainen, kein sofortiger Abbruch: wie lange es dauert, ist die Größe dieses Rückstaus plus postStop.
  • Der postStop des Singletons läuft.
  • Erst dann versucht der Manager Acquire erneut. Neu zu erwerben, während die alte Instanz noch drainiert, hat den Manager früher dauerhaft eine Lease über gar keinen Singleton halten lassen.

Bedeutet: wenn das Lease-Backend kurz hickst und sie kurzfristig widerruft, startet der Singleton neu während der Erholung — gleicher Effekt wie ein kurzer Actor-Neustart.

Wichtig, in welche Richtung die Ehrlichkeit hier schneidet: auf diesem Pfad ist Promptheit die Sicherheitseigenschaft, und ein Drain ist nicht prompt. Ein Singleton mit langer Mailbox und langsamem postStop läuft nach dem Widerruf seiner Lease genau so lange weiter — ein Fenster, in dem ein anderer Halter schon gestartet sein kann. Halte postStop in einem Singleton mit Lease kurz, und behandle den Widerruf als den Moment, ab dem die Instanz nicht mehr autoritativ ist, nicht als den Moment, in dem sie stoppt.

K8s-API-Ausfall → keine Lease-Renewals → Singleton verliert irgendwann die Lease
→ Singleton stoppt überall
→ kein Singleton verfügbar, bis K8s-API sich erholt

Das Lease-Backend wird zum SPOF. Für typische Cluster ist die K8s-API-Uptime viel höher als die des restlichen Systems, aber das ist eine echte Überlegung. Plane den seltenen Fall ein.

A's Lease hat TTL 30s.
A stürzt ab — kein Renew passiert.
Nach 30s läuft die Lease ab.
B erwirbt + spawnt Singleton.

Failover-Fenster = Lease-TTL. Konfigurierbar über das ttlMs-Feld der Lease.

Kürzere TTL = schnelleres Failover, aber mehr Renew-Verkehr. Typisch: 15-30 Sekunden.

Sekunden 0-30: A's Lease noch gültig (er ist abgestürzt, aber TTL läuft noch)
B kann nicht acquiren; nirgendwo ein Singleton
Nachrichten an den Singleton landen in Dead Letters
Sekunden 30+: B erwirbt; Singleton spawnt
Wenn Singleton ein PersistentActor ist, läuft Recovery
Nachrichten werden wieder verarbeitet

Während der Lücke scheitern Nachrichten an den Singleton — sie routen zu einem gestoppten Manager und landen in Dead Letters.

Für Workloads, bei denen das inakzeptabel ist, erwäge:

  • Niedrigeres TTL (15 s gibt schnelleres Failover, mehr Renewals).
  • Pufferung beim Sender — der Singleton-Proxy puffert, solange der Cluster keinen Host hat, und leert den Puffer, sobald einer da ist. Der Puffer ist gedeckelt (withBufferSize, Default 1000); jenseits des Limits gehen Nachrichten mit einer Warnung in die Dead Letters, damit eine Lücke, die sich nie schließt, nicht unbegrenzt wachsen kann.
  • Andere Architektur — Singletons sind inhärent seriell-bei-Failover.
Singleton + LeaseSharding + Lease
Was geschützt wirdSingleton-EinzigartigkeitKoordinator-Einzigartigkeit
Was bei Lease-Verlust stopptDer Singleton-ActorAllokationen des Koordinators
Failover-Fenster-KostenSingleton unverfügbarNeue Shard-Allokationen werden in Warteschlange gestellt
Bestehende Arbeit während FailoverPausiertLäuft für bereits allokierte Shards weiter

Singleton-Failover ist disruptiver — der Singleton ist die Workload. Sharding-Failover betrifft nur neue Allokationen — existierende Entities verarbeiten weiter.