Zum Inhalt springen
Deutsch

Lease-API

Das Lease-Interface ist die Abstraktion des Frameworks für verteilte Locks. Implementierungen unterscheiden sich im Backing Store (in-memory, K8s, etcd); der Vertrag ist identisch.

interface Lease {
acquire(): Promise<boolean>;
release(): Promise<void>;
checkAlive(): boolean;
onLost(handler: (reason: string) => void): () => void;
}

Drei Methoden, die Arbeit verrichten + eine, die einen Callback registriert.

const got = await lease.acquire();
if (got) {
// Wir halten den Lease — fahre mit der Leader-only-Arbeit fort
} else {
// Jemand anders hat ihn — backoff und später erneut versuchen
}

Die Semantik:

  • Resolved mit true, wenn der Lease erfolgreich acquired wurde.
  • Resolved mit false, wenn ein anderer Halter den Lease besitzt.
  • Rejected bei transienten Fehlern (Netzwerk, Backend nicht verfügbar).

Implementierungen retryen typischerweise intern bis zu acquireRetries-mal, bevor sie mit false resolven. Ein false-Ergebnis heißt “ein anderer Halter hat ihn definitiv”; ein Reject heißt “ich weiß es nicht”.

acquire() ist idempotent wenn dieser Caller den Lease bereits hältacquire() zweimal hintereinander vom selben Owner aufzurufen, gibt beide Male true zurück.

await lease.release();

Eigentum freiwillig abgeben. Aufruf ohne gehaltenen Lease ist ein No-op — kein Fehler. Resolved, sobald das Backend das Release bestätigt hat.

Das Framework ruft release():

  • Wenn der Singleton Manager aufhört, Leader zu sein (graceful Übergabe an einen anderen Node).
  • Wenn das Actor System via Coordinated Shutdown herunterfährt.

Für den nicht-grazilen Fall — Prozess-Crash — übernimmt die TTL des Backends die automatische Bereinigung; es wird kein release gesendet.

if (lease.checkAlive()) {
// Wir halten den Lease noch — fahre fort
}

Ein synchroner, lokaler Check. Kein Netzwerk-Roundtrip. Gibt den jüngsten Wissensstand des Halters zurück: “Halte ich das noch?”

Vom Framework genutzt, um eigentumsabhängige Arbeit zu gaten — z. B. ruft der Koordinator vor dem Vergeben einer Shard-Allocation checkAlive() und bricht ab, wenn false zurückkommt.

Implementierungen tracken Eigentum lokal; die Renewal-Schleife des Backends aktualisiert das lokale Flag. Das heißt, checkAlive() spiegelt bis zu einem verpassten Renewal an Veraltung wider — ein Sub-Sekunden-Fenster, in dem der Lease tatsächlich weg sein könnte, checkAlive() aber noch true liefert.

Für absolute Gewissheit nimm onLost(...) und reagiere auf die Benachrichtigung, statt zu pollen.

const unsubscribe = lease.onLost((reason) => {
console.log(`Lease verloren: ${reason}`);
// Leader-only-Arbeit sofort stoppen
});
// Später: unsubscribe();

Registriere einen Callback, der feuert, wenn das Eigentum unerwartet verloren geht:

  • Das Backend hat gemeldet, der Lease wurde von einem anderen Halter übernommen.
  • Die TTL ist ohne erfolgreiches Renewal abgelaufen (z. B. Netzwerk-Partition).
  • Das Backend selbst hat eine Zustands-Inkonsistenz gemeldet.

onLost feuert einmal pro Verlust. Danach gibt checkAlive() false zurück, und acquire() ist nötig, um das Eigentum zurückzugewinnen.

Der Handler sollte State, der vom Eigentum abhängt, sofort fallen lassen — Arbeit stoppen, Locks freigeben, interessierte Actors benachrichtigen. Warte nicht auf teure Operationen; der Lease ist weg, und jeder andere Halter handelt vielleicht schon.

Gibt eine Unsubscribe-Funktion zurück — aufrufen, um den Handler zu entfernen, wenn du ihn nicht mehr brauchst.

Für einen Singleton mit Lease:

ja

true

false

nein, verloren

LeaderChanged-Event feuert

ist dieser Node

jetzt Leader?

lease.acquire

acquired?

Singleton spawnen

onLost registrieren

nach acquireRetryDelay

erneut versuchen

Singleton stoppen

lease.release

onLost feuert

Singleton stoppen

nächstes LeaderChanged abwarten

Gleiches Muster für den Sharding-Koordinator:

  1. lease.acquire vor Verarbeitung von Allocation-Anfragen.
  2. lease.checkAlive vor dem Vergeben jeder Allocation.
  3. onLost → ausstehende Allocations ablehnen, Koordinator stoppen.
import type { Lease, LeaseSettings } from 'actor-ts';
class EtcdLease implements Lease {
private alive = false;
private onLostHandlers = new Set<(reason: string) => void>();
private renewTimer: NodeJS.Timeout | null = null;
constructor(private readonly settings: LeaseSettings & { /* etcd-spezifisch */ }) {}
async acquire(): Promise<boolean> {
// Versuche, den etcd-Key atomar von leer auf diesen Owner zu CAS-en.
// Starte bei Erfolg einen Renewal-Timer.
// ...
}
async release(): Promise<void> {
// Renewal-Timer stoppen.
// Den etcd-Key von diesem Owner auf leer CAS-en.
// ...
}
checkAlive(): boolean {
return this.alive;
}
onLost(handler: (reason: string) => void): () => void {
this.onLostHandlers.add(handler);
return () => this.onLostHandlers.delete(handler);
}
private fireOnLost(reason: string): void {
this.alive = false;
for (const h of this.onLostHandlers) {
try { h(reason); } catch { /* schlucken */ }
}
}
}

Drei Dinge, die jedes Backend richtig hinkriegen muss:

  1. Atomarität bei acquire — zwei nebenläufige acquire()-Aufrufe verschiedener Owner müssen genau einen Gewinner produzieren. Das Konsistenzmodell des Backends muss das liefern (CAS, Paxos, Raft-backed).
  2. Periodisches Renewal — den Lease im Backend am Leben halten. Konfigurierbares Intervall, typischerweise ttl / 3.
  3. onLost-Genauigkeit — feuern, wenn das Eigentum tatsächlich übergeht, inklusive des TTL-Ablauf-Falls.

Teste die Implementierung gegen:

  • Zwei nebenläufige Acquires von verschiedenen Ownern.
  • Netzwerk-Partition, bei der beide Seiten zu renewen versuchen.
  • Halter-Crash + neues Acquire nach TTL.
  • Halter-Prozess-Pause (z. B. GC-Stall) länger als die TTL.

Die Lease-API-Referenz deckt den vollständigen Vertrag ab.