Durable storage
이 콘텐츠는 아직 번역되지 않았습니다.
By default, DistributedData replicas live only in memory. A
full-cluster restart (every node down at once) loses all the
state — every key starts from empty again.
For state that must survive a cold start, hand
DistributedData a durable backend with
DistributedDataOptions.withDurableStore(...):
import { DistributedDataId, DistributedDataOptions, InMemoryDurableStateStore,} from 'actor-ts';
// Any DurableStateStore works — InMemory here; a SQL / object-storage// backend in production (see "Store choices" below).const store = new InMemoryDurableStateStore();
const distributedDataOptions = DistributedDataOptions.create() .withGossipInterval(1_000) .withDurableStore(store);
const dd = system.extension(DistributedDataId).start( cluster, distributedDataOptions,);withDurableStore takes a DurableStateStore directly — not a
wrapper. Internally, DistributedData wraps it in a
DurableDistributedDataStore keyed by this node’s own replica id,
so durability is per-replica: each cluster member owns one
durable record.
What this gives you:
- On
preStart— before the replica joins gossip, it loads its persisted view from the store. - After every mutation — local
update, incoming gossip merge, anddeleteall re-save the view.
After a cold start, dd.get(key) returns the last-persisted value
— not undefined — and gossip re-merges across replicas from
there.
What gets persisted
Section titled “What gets persisted”There is no key whitelist. The replica’s entire local view
(every key it holds) is serialized and written as one durable
record per replica, under the id ddata|<replicaId>. Each value
is serialized via the CRDT’s own toJSON().
Because the whole view is one record, every mutation rewrites the whole record — the same full-state-rewrite trade-off durable-state actors have. For small, hot state (counters, flags, small sets) this is cheap; for a large view under a high mutation rate it costs bandwidth and write amplification (see the cautions below).
Store choices
Section titled “Store choices”Any DurableStateStore implementation
works — construct it exactly as you would for a durable-state actor,
then pass it to withDurableStore:
| Store | Use |
|---|---|
InMemoryDurableStateStore | Tests / dev — durable within the process, gone on exit. |
PostgresDurableStateStore | Cluster-shared SQL durability. |
MariaDbDurableStateStore | The MySQL-family SQL alternative. |
ObjectStorageDurableStateStore | Filesystem- or S3-backed records. |
See Durable state for each store’s construction options — DistributedData reuses the same stores, so there’s nothing DD-specific to configure.
Per-node local vs shared backend
Section titled “Per-node local vs shared backend”The store is keyed by replica id, so the two deployment shapes differ in where each replica’s record lives, not in what a node loads:
Per-node local store (e.g. object storage at a local path): node-A's store ← ddata|node-A node-B's store ← ddata|node-B cold start → each node loads its own record; gossip reconvergesShared backend (one Postgres / one S3 bucket): shared store ← ddata|node-A, ddata|node-B, … (one record per replica) cold start → each node loads its own record; gossip reconverges- Per-node local — full local recovery, partition-tolerant; but a destroyed node’s record is gone (gossip + survivors restore the values, since CRDTs converge).
- Shared — every replica’s record survives in one place even if the node is destroyed; costs a round trip to the shared store at startup and makes that store a shared dependency.
For most apps either is fine — CRDT convergence means losing one node’s record is rarely fatal.
Low-level: DurableDistributedDataStore directly
Section titled “Low-level: DurableDistributedDataStore directly”withDurableStore is the path you almost always want. The wrapper
it builds is also usable on its own — a positional constructor over
a DurableStateStore plus a replica id, with load() / save() /
clear():
import { DurableDistributedDataStore, GCounter } from 'actor-ts';import { InMemoryDurableStateStore } from 'actor-ts';
const store = new InMemoryDurableStateStore();const durable = new DurableDistributedDataStore(store, 'replica-a');
const view = new Map([['hits', GCounter.empty().increment('replica-a', 5)]]);await durable.save(view); // persist the whole view
const restored = await durable.load(); // Map<string, Crdt> (empty if nothing stored)await durable.clear(); // forget this replica's recordThis is what the framework uses under the hood; reach for it directly only for tooling or tests.
Startup flow
Section titled “Startup flow”const distributedDataOptions = DistributedDataOptions.create() .withDurableStore(store);const dd = system.extension(DistributedDataId).start( cluster, distributedDataOptions,);
// On preStart, before joining gossip:// 1. load() this replica's record from the durable store.// 2. decode each entry into its CRDT.// 3. seed the local view.// 4. then join the gossip layer.
const value = dd.get('hits'); // already reflects the persisted viewStartup is bounded by the durable store’s read speed. For a local store with a handful of keys, sub-millisecond; for a shared SQL / S3 backend, single-digit seconds.
When NOT to use durable storage
Section titled “When NOT to use durable storage”For at-rest encryption or compression, configure it on the underlying store where the backend supports it (e.g. the object-storage store — see Object storage encryption).
Where to next
Section titled “Where to next”- Distributed data overview — the bigger picture.
- Replication — how state propagates between in-memory replicas.
- Durable state —
the
DurableStateStorebackends you plug in here. - PersistentActor — the event-sourced alternative for state with frequent updates.
