Zum Inhalt springen
Deutsch

Durable State

DurableStateActor<Cmd, S> ist das “Ich will einfach, dass der aktuelle Wert überlebt”-Persistenz-Modell. Kein Event-Log, kein Replay — nur ein Snapshot des States, bei jedem persist(newState) überschrieben.

DurableStateActor

onReceive(cmd)

this.state lesen

next = transform(...)

await this.persist(next)

revision++

Durable State Store

Verglichen mit PersistentActor:

AspektPersistentActorDurableStateActor
Was gespeichert wirdJedes je geschriebene EventDer aktuelle State-Snapshot
RecoveryEvents abspielenSnapshot laden
Storage-KostenWächst mit EventsKonstant pro Actor
HistorieJaNein
Audit / Time TravelJaNein
Gleichzeitige WritesSequenziellOptimistisch (Revisions-Check)

Wähle Durable State, wenn die Historie nicht nützlich ist — Feature Flags, last-known Configs, “aktuelle Cart-Inhalte” ohne den Audit-Trail.

import { DurableStateActor, type DurableStateSettings, ActorSystem, Props } from 'actor-ts';
import { InMemoryDurableStateStore } from 'actor-ts';
import { match } from 'ts-pattern';
type CartCmd =
| { kind: 'add'; sku: string }
| { kind: 'remove'; sku: string }
| { kind: 'view'; replyTo: ActorRef<State> };
interface State { items: string[]; }
class Cart extends DurableStateActor<CartCmd, State> {
constructor(settings: DurableStateSettings<State>) { super(settings); }
override async onReceive(cmd: CartCmd): Promise<void> {
await match(cmd)
.with({ kind: 'add' }, (c) => this.persist({ items: [...this.state.items, c.sku] }))
.with({ kind: 'remove' }, (c) => this.persist({ items: this.state.items.filter(s => s !== c.sku) }))
.with({ kind: 'view' }, (c) => { c.replyTo.tell(this.state); })
.exhaustive();
}
}
// Setup:
const system = ActorSystem.create('demo');
const store = new InMemoryDurableStateStore();
const cart = system.spawn(
Props.create(() => new Cart({
persistenceId: 'cart-user-42',
store,
emptyState: () => ({ items: [] }),
})),
'cart',
);
cart.tell({ kind: 'add', sku: 'book-1' });
cart.tell({ kind: 'add', sku: 'book-2' });
// Nach einem Neustart: `this.state.items` ist wieder ['book-1', 'book-2'].
interface DurableStateSettings<S> {
persistenceId: string;
store: DurableStateStore;
emptyState: () => S;
}

Drei Felder:

  • persistenceId — der Schlüssel, unter dem der State gespeichert ist. Wie bei PersistentActor eine ID pro logischer Entity (cart-user-42, flags-region-eu, …).
  • store — die DurableStateStore-Implementierung (in-memory, SQLite, Object Storage, benutzerdefiniert).
  • emptyState() — Factory, die aufgerufen wird, wenn noch kein Eintrag existiert (erste Ausführung, gelöschter State). Stellt den Initialwert bereit.

Reiche sie durch die Props.create(() => new Cart({...}))-Factory. Die Settings können pro Actor-Inkarnation variieren (unterschiedliche IDs für unterschiedliche User, derselbe Store).

Innerhalb der Actor-Handler:

this.state // aktueller State-Wert — überall lesbar
this.revision // monoton wachsender Counter, wird bei jedem Persist erhöht
this.persist(s) // überschreibt den gespeicherten State mit `s`, gibt Promise<void> zurück

this.state ist synchron — das Framework lädt den State in preStart, und state gibt zurück, was gerade im Speicher steht. Vor dem ersten Persist (oder Load) gibt es emptyState() zurück.

this.persist(next) schreibt den neuen State in den Store mit der aktuellen Revision + 1. Kehrt zurück, sobald der Store bestätigt. Innerhalb von onReceive warte mit await darauf, bevor du den nächsten State als autoritativ behandelst.

try {
await this.persist(next);
} catch (e) {
if (e instanceof DurableStateConcurrencyError) {
// Ein anderer Writer war schneller — neu laden und erneut versuchen oder zum User durchreichen.
}
}

Wenn zwei Writer gleichzeitig dieselbe persistenceId aktualisieren, lehnt der Revisions-Check des Stores den zweiten mit DurableStateConcurrencyError ab. Strategien:

  • Das Problem vermeiden — stelle sicher, dass immer nur ein Actor zur selben Zeit in eine gegebene persistenceId schreibt. Das ist meistens trivial: jeder cart-user-42 hat einen Actor auf einem Node (per Sharding oder Singleton).
  • Neu laden und erneut versuchen — den Fehler fangen, State neu laden, den neuen Wert neu berechnen, erneut persistieren. Funktioniert, wenn die Operation idempotent ist.
  • Zum Aufrufer durchreichen — mit einem Fehler antworten; den Aufrufer entscheiden lassen, ob er erneut versuchen will.

Bei den meisten Actor-System-Mustern sollten gleichzeitige Writes nicht passieren — eine Entity pro persistenceId, über Routing oder Sharding adressiert. Wenn du Concurrency-Fehler in Produktion siehst, bedeutet das meist, dass zwei Actors denselben Key schreiben, was ein Routing-Bug ist.

Drei Signale, dass du das richtige Werkzeug gewählt hast:

  • Die State-Form ist einfach (ein einzelnes Objekt, eine kleine Map). Das Ganze zu überschreiben ist billig.
  • Du brauchst keinen Event-Stream — keine Projektionen, kein Audit-Log, keine “zeig mir, wie wir hier gelandet sind”- Anforderungen.
  • Reads dominieren Writes — jedes Read ist ein synchrones this.state, kein Replay.

Drei Signale, dass du stattdessen zu PersistentActor greifen solltest:

  • Die Historie ist wichtig — Auditing, regulatorisch, “zeig dem User ein Changelog.”
  • Der State ist groß und Änderungen sind klein — den gesamten State bei jeder Änderung zu schreiben ist verschwenderisch; kleine Events anzuhängen ist billiger.
  • Du willst Projektionen — Read-Side-Views, die den Event-Stream brauchen.

Wie bei PersistentActor unterstützt Durable State Schema-Evolution durch einen Adapter:

import { StateAdapter } from 'actor-ts';
class V1ToV2Adapter implements StateAdapter<StateV2> {
upcast(stored: unknown, version: number): StateV2 {
if (version === 1) return migrate(stored as StateV1);
return stored as StateV2;
}
}
class Cart extends DurableStateActor<...> {
protected stateAdapter() { return new V1ToV2Adapter(); }
}

Der persistierte Eintrag wird in einen { _v, _t, _e }-Envelope verpackt; beim Laden läuft das upcast des Adapters, um ältere Versionen zu migrieren. Siehe Migration im Überblick für die vollständige Story.

Per-Actor-Overrides sind verfügbar:

class Sensitive extends DurableStateActor<...> {
protected encryption() { return { algorithm: 'aes-gcm', keyId: 'k1' }; }
protected compression() { return { algorithm: 'gzip' }; }
}

Werden von Stores berücksichtigt, die sie implementieren (Object-Storage mit Verschlüsselung, etc.); ignoriert von Stores, die das nicht tun (In-Memory, SQLite). Siehe Object-Storage-Verschlüsselung für die Durable-State-Verschlüsselungs-Story.

Die DurableStateActor-API-Referenz deckt die vollständige Oberfläche ab.