Перейти к содержимому
Русский

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, and delete all 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.

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

Any DurableStateStore implementation works — construct it exactly as you would for a durable-state actor, then pass it to withDurableStore:

StoreUse
InMemoryDurableStateStoreTests / dev — durable within the process, gone on exit.
PostgresDurableStateStoreCluster-shared SQL durability.
MariaDbDurableStateStoreThe MySQL-family SQL alternative.
ObjectStorageDurableStateStoreFilesystem- 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.

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 reconverges
Shared 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 record

This is what the framework uses under the hood; reach for it directly only for tooling or tests.

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 view

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

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