Zum Inhalt springen
Deutsch

SQLite-Snapshot-Store

SqliteSnapshotStore persistiert Snapshots in eine SQLite-Datei — durable, null Dependency auf Bun, Peer-Dep auf Node. Paare es mit SqliteJournal für das Standard-Single-Node-Produktions-Setup.

import {
SqliteJournal,
SqliteJournalOptions,
SqliteSnapshotStore,
SqliteSnapshotStoreOptions,
ActorSystem,
ActorSystemOptions,
} from 'actor-ts';
const sqliteJournalOptions = SqliteJournalOptions.create()
.withPath('/var/lib/my-app/events.db')
.withWal(true);
const sqliteSnapshotStoreOptions = SqliteSnapshotStoreOptions.create().withPath('/var/lib/my-app/snapshots.db');
const actorSystemOptions = ActorSystemOptions.create().withPersistence({
journal: new SqliteJournal(sqliteJournalOptions),
snapshotStore: new SqliteSnapshotStore(sqliteSnapshotStoreOptions),
});
const system = ActorSystem.create('my-app', actorSystemOptions);
type SqliteSnapshotStoreOptions = {
path?: string; // Dateipfad oder ':memory:'
snapshotsTable?: string; // Default 'snapshots'
keepN?: number; // max. Snapshots pro persistenceId, beim Speichern beschnitten (Default 3)
busyTimeoutMs?: number; // Lock-Wartebudget, Default 1000; 0 = sofort scheitern
driver?: SqliteDriver;
};

path, busyTimeoutMs und driver verhalten sich genau wie beim SQLite-Journal — einschließlich des Grundes, warum busyTimeoutMs überhaupt einen vom Framework gesetzten Default hat: die Treiber liefern unterschiedliche mit (0 ms bei bun:sqlite und node:sqlite, 5000 ms bei better-sqlite3), und das Warten ist synchron, blockiert also die Event-Loop. Es gibt hier keine wal-Option; stattdessen bietet der Snapshot-Store keepN — die maximale Anzahl an Snapshots, die pro persistenceId behalten werden, wobei ältere bei jedem Speichern beschnitten werden (Default 3).

const sqliteSnapshotStoreOptions = SqliteSnapshotStoreOptions.create()
.withPath('/var/lib/my-app/snapshots.db')
.withBusyTimeoutMs(1000);
new SqliteSnapshotStore(sqliteSnapshotStoreOptions)
CREATE TABLE snapshots (
persistence_id TEXT NOT NULL,
sequence_nr INTEGER NOT NULL,
payload TEXT NOT NULL,
timestamp INTEGER NOT NULL,
PRIMARY KEY (persistence_id, sequence_nr)
);

Gleiches Pattern wie das Journal: nach (persistence_id, sequence_nr) geschlüsselt. Das Framework liest die höchste sequence_nr für eine gegebene persistence_id bei loadLatest.

// Produktion:
{
const sqliteJournalOptions = SqliteJournalOptions.create().withPath('/var/lib/events.db');
const sqliteSnapshotStoreOptions = SqliteSnapshotStoreOptions.create().withPath('/var/lib/snapshots.db');
journal: new SqliteJournal(sqliteJournalOptions),
snapshotStore: new SqliteSnapshotStore(sqliteSnapshotStoreOptions),
}

Das Journal und der Snapshot-Store sind unabhängige Dateien. Du kannst sie auch in dieselbe Datei mit unterschiedlichen Tabellennamen packen — aber zwei Dateien halten Operationen einfacher (unabhängiges Backup, unabhängiges Vacuum).

Für Multi-Node-Deployments, in denen das Journal Cassandra ist, kannst du trotzdem SQLite für Snapshots verwenden, wenn jede Entity immer auf demselben Node wiederhergestellt wird. Aber für Sharded Entities (die zwischen Nodes wandern) muss der Snapshot-Store auch geteilt sein — siehe Object-Storage-Snapshot-Store.

  • Snapshot-Write — single INSERT, ~100 μs. Dominiert von der Serialisierung, nicht von SQLite-I/O.
  • Snapshot-Read — single SELECT, Sub-Millisekunde.

Für sehr große Snapshots (Multi-MB-State) ist die Serialisierung der Bottleneck. Überlege Kompression (Object-Storage-Kompression) oder eine andere Snapshot-Strategie.

Die SqliteSnapshotStore-API- Referenz deckt die vollständigen Optionen ab.