DynamoDB
Das DynamoDB-Backend liefert alle drei Persistence-Komponenten gegen Amazon
DynamoDB, über @aws-sdk/client-dynamodb:
DynamoDbJournal— das Event-Journal fürPersistentActors.DynamoDbSnapshotStore— Snapshots, um die Recovery zu begrenzen.DynamoDbDurableStateStore— Key-Value-Durable-State fürDurableStateActors.
Es hat die stärkste Optimistic Concurrency aller mitgelieferten Backends — siehe Concurrency-Modell — und ist vollständig serverless: keine Instanz zu betreiben, standardmäßig On-Demand-Kapazität.
Installation
Abschnitt betitelt „Installation“@aws-sdk/client-dynamodb ist eine optionale Peer-Dependency:
bun add @aws-sdk/client-dynamodbEs ist dieselbe SDK-Familie, die das S3-Object-Storage-Backend schon nutzt — wer das einsetzt, kennt die Runtime-Situation also bereits. Das Framework importiert sie lazy, erst wenn ein DynamoDB-Store zum ersten Mal genutzt wird.
Einrichtung
Abschnitt betitelt „Einrichtung“Registriere Journal + Snapshot-Store an der PersistenceExtension und erhalte
einen einsatzbereiten Durable-State-Store. Setz die Verbindung einmal am
Composite, dann erben sie alle drei Komponenten:
import { ActorSystem, ActorSystemOptions, DynamoDbDurableStateStoreOptions, DynamoDbJournalOptions, DynamoDbSnapshotStoreOptions, PersistenceExtensionId, RegisterDynamoDbPluginsOptions, registerDynamoDbPlugins,} from 'actor-ts';
const systemOptions = ActorSystemOptions.create() // Die DynamoDB-Plugins als aktives Journal + Snapshot-Store auswählen. .withConfig({ 'actor-ts': { persistence: { journal: { plugin: 'actor-ts.persistence.journal.dynamodb' }, 'snapshot-store': { plugin: 'actor-ts.persistence.snapshot-store.dynamodb' }, }, }, });const system = ActorSystem.create('my-app', systemOptions);
const ext = system.extension(PersistenceExtensionId);const dynamoDbSnapshotStoreOptions = DynamoDbSnapshotStoreOptions.create() .withKeepN(3);const registerOptions = RegisterDynamoDbPluginsOptions.create() .withRegion('eu-central-1') .withJournal(DynamoDbJournalOptions.create() /* .withEventsTable(...) */) .withSnapshotStore(dynamoDbSnapshotStoreOptions) .withDurableStateStore(DynamoDbDurableStateStoreOptions.create() /* .withTable(...) */);const { durableStateStore } = registerDynamoDbPlugins(ext, registerOptions);Credentials kommen aus der Standard-AWS-Provider-Kette (Umgebung, Shared Config,
IAM-Rolle) — daran ist nichts actor-ts-spezifisch. Mit
withClientConfig({ credentials }) überschreibst du das.
Lokale Entwicklung
Abschnitt betitelt „Lokale Entwicklung“dynamodb-local braucht einen Endpoint-Override, und das SDK signiert keinen
Request ohne Credentials, auch wenn der Emulator sie ignoriert:
const dynamoDbJournalOptions = DynamoDbJournalOptions.create() .withRegion('eu-central-1') .withEndpoint('http://localhost:8000') .withClientConfig({ credentials: { accessKeyId: 'local', secretAccessKey: 'local' } });Wann du es einsetzt
Abschnitt betitelt „Wann du es einsetzt“- Du bist auf AWS und willst keine Datenbank betreiben. Keine Instanz, kein Connection-Pool, kein Patchen.
- Du willst die stärkste Append-Garantie, die zu haben ist. Der transaktionale Append ist atomar — das schaffen nicht einmal die relationalen Backends für einen Multi-Event-Batch unter Contention.
- Dein Schreibmuster ist stoßweise. On-Demand-Billing braucht keine Kapazitätsplanung.
Konfiguration
Abschnitt betitelt „Konfiguration“type DynamoDbConnection = { region?: string; // eu-central-1, … endpoint?: string; // http(s)-URL — dynamodb-local, LocalStack clientConfig?: Record<string, unknown>; // { credentials, maxAttempts, … } operations?: DynamoDbOperations; // vorgebaute / geteilte Fassade};
type DynamoDbTableProvisioning = { autoCreateTables?: boolean; // Default true billingMode?: 'PAY_PER_REQUEST' | 'PROVISIONED'; // Default On-Demand provisionedThroughput?: { readCapacityUnits: number; writeCapacityUnits: number }; tableReadyTimeoutMs?: number; // Default 30_000};
interface DynamoDbJournalOptions extends DynamoDbConnection, DynamoDbTableProvisioning { eventsTable?: string; // Default 'actor_ts_events'}interface DynamoDbSnapshotStoreOptions extends DynamoDbConnection, DynamoDbTableProvisioning { snapshotsTable?: string; // Default 'actor_ts_snapshots' keepN?: number; // neueste N pro pid; Default 3, <=0 behält alle}interface DynamoDbDurableStateStoreOptions extends DynamoDbConnection, DynamoDbTableProvisioning { table?: string; // Default 'actor_ts_durable_state'}Die Optionen werden bei der Konstruktion des Stores validiert. Zwei Regeln
lohnen sich besonders: ein Tabellenname wird gegen DynamoDBs eigenen Zeichensatz
und die Längengrenze geprüft, und Kapazitätseinheiten werden abgelehnt, solange
billingMode nicht PROVISIONED ist — AWS ignoriert sie sonst still, was den
Fehler unsichtbar macht.
Tabellen
Abschnitt betitelt „Tabellen“| Tabelle | Partition Key | Sort Key | Inhalt |
|---|---|---|---|
actor_ts_events | pid | seq (N) | ein Item pro Event; seq = 0 ist die Kompaktierungs-Marke |
actor_ts_snapshots | pid | seq (N) | ein Item pro Snapshot |
actor_ts_durable_state | pid | — | ein Item pro persistenceId |
Mit autoCreateTables (dem Default) legt jeder Store seine Tabelle beim ersten
Zugriff an und wartet, bis sie ACTIVE ist — CreateTable kehrt zurück,
während die Tabelle noch CREATING ist, und jede Operation darauf scheitert bis
zum Umschalten. Das Warten passiert im Lazy-Init: der erste Aufrufer blockiert,
alle dahinter bekommen eine funktionierende Tabelle.
Concurrency-Modell
Abschnitt betitelt „Concurrency-Modell“- Der Journal-Append ist eine atomare Transaktion. Alle Events gehen in ein
einzelnes
TransactWriteItems, jedesPutmitConditionExpression: attribute_not_exists(pid)— „nur wenn dieses(pid, seq)-Item nicht existiert”. Ein konkurrierender Writer kann damit keinen partiellen Append gewinnen: die Transaktion ist ganz oder gar nicht, und ihr Abbruch wird zumJournalConcurrencyError. Der vorausgehende Head-Read ist nur eine Optimierung, die den häufigen veralteten Append zu einer billigen Query statt einer abgelehnten Transaktion macht. - Durable-State-CAS ist ein nativer konditionaler Write —
PutItem … attribute_not_exists(pid)zum Anlegen,UpdateItem … revision = :expectedzum Aktualisieren — ohne Transaktion und ohne Rücklesen. - Die Kompaktierungs-Marke steigt nur. Sie wird mit
attribute_not_exists(deletedTo) OR deletedTo < :valueaktualisiert, alsoGREATESTals Bedingung formuliert; ein niedrigerer Wert wird abgelehnt, und diese Ablehnung ist das erwartete Ergebnis, kein Fehler.
Fallstricke
Abschnitt betitelt „Fallstricke“Wie geht’s weiter
Abschnitt betitelt „Wie geht’s weiter“- MongoDB — das andere Nicht-SQL-Backend, mit indizierter Tag-Query.
- PostgreSQL — wenn du doch eine relationale Datenbank betreiben willst.
- Durable State — der Contract, zu dem DynamoDB am besten passt.
- Snapshots — begrenzt den Recovery-Scan.
