Zum Inhalt springen
Deutsch

DynamoDB

Das DynamoDB-Backend liefert alle drei Persistence-Komponenten gegen Amazon DynamoDB, über @aws-sdk/client-dynamodb:

  • DynamoDbJournal — das Event-Journal für PersistentActors.
  • DynamoDbSnapshotStore — Snapshots, um die Recovery zu begrenzen.
  • DynamoDbDurableStateStore — Key-Value-Durable-State für DurableStateActors.

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.

@aws-sdk/client-dynamodb ist eine optionale Peer-Dependency:

Terminal-Fenster
bun add @aws-sdk/client-dynamodb

Es 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.

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.

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' } });
  • 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.
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.

TabellePartition KeySort KeyInhalt
actor_ts_eventspidseq (N)ein Item pro Event; seq = 0 ist die Kompaktierungs-Marke
actor_ts_snapshotspidseq (N)ein Item pro Snapshot
actor_ts_durable_statepidein Item pro persistenceId

Mit autoCreateTables (dem Default) legt jeder Store seine Tabelle beim ersten Zugriff an und wartet, bis sie ACTIVE istCreateTable 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.

  • Der Journal-Append ist eine atomare Transaktion. Alle Events gehen in ein einzelnes TransactWriteItems, jedes Put mit ConditionExpression: 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 zum JournalConcurrencyError. 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 WritePutItem … attribute_not_exists(pid) zum Anlegen, UpdateItem … revision = :expected zum Aktualisieren — ohne Transaktion und ohne Rücklesen.
  • Die Kompaktierungs-Marke steigt nur. Sie wird mit attribute_not_exists(deletedTo) OR deletedTo < :value aktualisiert, also GREATEST als Bedingung formuliert; ein niedrigerer Wert wird abgelehnt, und diese Ablehnung ist das erwartete Ergebnis, kein Fehler.
  • 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.