Durable State
DurableStateActor<Command, 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.
Verglichen mit PersistentActor:
| Aspekt | PersistentActor | DurableStateActor |
|---|---|---|
| Was gespeichert wird | Jedes je geschriebene Event | Der aktuelle State-Snapshot |
| Recovery | Events abspielen | Snapshot laden |
| Storage-Kosten | Wächst mit Events | Konstant pro Actor |
| Historie | Ja | Nein |
| Audit / Time Travel | Ja | Nein |
| Gleichzeitige Writes | Sequenziell | Optimistisch (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.
Ein minimales Beispiel
Abschnitt betitelt „Ein minimales Beispiel“import { DurableStateActor, DurableStateOptions, ActorSystem } from 'actor-ts';import { InMemoryDurableStateStore } from 'actor-ts';import { match } from 'ts-pattern';
type AddCommand = { kind: 'add'; sku: string };type RemoveCommand = { kind: 'remove'; sku: string };type ViewCommand = { kind: 'view'; replyTo: ActorRef<State> };type CartCommand = AddCommand | RemoveCommand | ViewCommand;
type State = { items: string[]; };
class Cart extends DurableStateActor<CartCommand, State> { constructor(options: DurableStateOptions<State>) { super(options); }
override async onCommand(command: CartCommand): Promise<void> { await match(command) .with({ kind: 'add' }, (c) => this.onAdd(c)) .with({ kind: 'remove' }, (c) => this.onRemove(c)) .with({ kind: 'view' }, (c) => this.onView(c)) .exhaustive(); }
private onAdd(c: AddCommand): Promise<void> { return this.persist({ items: [...this.state.items, c.sku] }); }
private onRemove(c: RemoveCommand): Promise<void> { return this.persist({ items: this.state.items.filter(s => s !== c.sku) }); }
private onView(c: ViewCommand): void { c.replyTo.tell(this.state); }}
// Setup:const system = ActorSystem.create('demo');const store = new InMemoryDurableStateStore();
const durableStateOptions = DurableStateOptions.create<State>() .withPersistenceId('cart-user-42') .withStore(store) .withEmptyState(() => ({ items: [] }));const cart = system.spawn( () => new Cart(durableStateOptions), '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'].Die Settings
Abschnitt betitelt „Die Settings“type DurableStateOptionsType<S> = { persistenceId: string; store: DurableStateStore; emptyState: () => S;};Drei Felder:
persistenceId— der Schlüssel, unter dem der State gespeichert ist. Wie beiPersistentActoreine ID pro logischer Entity (cart-user-42,flags-region-eu, …) — und denselben Regeln unterworfen, was eine ID enthalten darf: nicht leer, höchstens 255 Zeichen, kein/oder\, keine Control-Zeichen.DurableStateOptionsValidatorprüft sie im Konstruktor — früher alspreStart, weil eine ID, die nie einen Eintrag adressieren kann, es wert ist, zurückgewiesen zu werden, bevor der Actor an einen Store gebunden wird — und meldet einen Verstoß alsOptionsErrorauf dem FeldpersistenceId.store— dieDurableStateStore-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 () => new Cart({...})-Factory.
Die Settings können pro Actor-Inkarnation variieren (unterschiedliche
IDs für unterschiedliche User, derselbe Store).
State-Zugriff + Persistenz
Abschnitt betitelt „State-Zugriff + Persistenz“Innerhalb der Actor-Handler:
this.state // aktueller State-Wert — überall lesbarthis.revision // monoton wachsender Counter, wird bei jedem Persist erhöhtthis.persist(s) // überschreibt den gespeicherten State mit `s`, gibt Promise<DurableStateRecord<S>> zurückthis.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 onCommand warte mit await darauf,
bevor du den nächsten State als autoritativ behandelst.
State wird auf jedem Backend im getaggten JSON-Tree-Format
gespeichert, sodass Date / Map / Set / bigint /
Uint8Array als echte Instanzen round-trippen — siehe
was Events und State enthalten dürfen.
Optimistische Concurrency
Abschnitt betitelt „Optimistische Concurrency“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
persistenceIdschreibt. Das ist meistens trivial: jedercart-user-42hat 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.
Wann Durable State gegen Persistent Actor gewinnt
Abschnitt betitelt „Wann Durable State gegen Persistent Actor gewinnt“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.
State-Migration
Abschnitt betitelt „State-Migration“Wie bei PersistentActor unterstützt Durable State Schema-Evolution
durch einen Adapter:
import { StateAdapter } from 'actor-ts';
const v1ToV2Adapter: StateAdapter<StateV2> = { manifest: () => 'Cart', toJournal: (state) => ({ manifest: 'Cart', version: 2, payload: state }), fromJournal: (stored) => stored.version === 1 ? migrate(stored.payload as StateV1) : stored.payload as StateV2,};
class Cart extends DurableStateActor<...> { protected stateAdapter() { return v1ToV2Adapter; }}Der persistierte Eintrag wird in einen { _v, _t, _e }-Envelope
verpackt; beim Laden läuft fromJournal des Adapters, um ältere
Versionen zu migrieren. Siehe
Migration im Überblick für
die vollständige Story.
Verschlüsselung + Kompression
Abschnitt betitelt „Verschlüsselung + Kompression“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.
Häufige Stolperfallen
Abschnitt betitelt „Häufige Stolperfallen“Wie geht’s weiter
Abschnitt betitelt „Wie geht’s weiter“- Persistenz im Überblick — die Entscheidung Durable State vs. Event Sourcing.
- PersistentActor — wenn die Historie wichtig ist.
- Migration im Überblick — State-Schemas weiterentwickeln.
- Object Storage — S3- + Filesystem-Backends für Durable State.
Die DurableStateActor-API-Referenz
deckt die vollständige Oberfläche ab.
