コンテンツにスキップ
日本語

Cached snapshot store

このコンテンツはまだ日本語訳がありません。

CachedSnapshotStore is a decorator — wrap any SnapshotStore to add a TTL-based read-through cache for loadLatest, backed by any Cache (in-memory, Redis, or Memcached).

import {
CachedSnapshotStore,
CachedSnapshotStoreOptions,
ObjectStorageSnapshotStore,
ObjectStorageSnapshotStoreOptions,
ActorSystem,
ActorSystemOptions,
} from 'actor-ts';
const objectStorageSnapshotStoreOptions = ObjectStorageSnapshotStoreOptions.create().withBackend(backend);
const underlying = new ObjectStorageSnapshotStore(
objectStorageSnapshotStoreOptions, // S3 / filesystem backend
);
const cachedSnapshotStoreOptions = CachedSnapshotStoreOptions.create()
.withCache(cache) // backing cache (typically Redis)
.withTtlMs(60_000);
const cached = new CachedSnapshotStore(
underlying,
cachedSnapshotStoreOptions, // optional TTL
);
const actorSystemOptions = ActorSystemOptions.create().withPersistence({
journal: myJournal,
snapshotStore: cached,
});
const system = ActorSystem.create('app', actorSystemOptions);

Three patterns:

  1. Slow underlying store — object storage with multi-hop network latency, encrypted state with expensive decryption.
  2. Frequent actor churn — sharded entities passivating / re-spawning constantly, each load re-reading the same snapshot.
  3. Recovery storms — full-cluster restart, every actor loads its snapshot at once. Cache reduces redundant loads when the same snapshot is queried during the storm.

For local SQLite-backed snapshots (sub-millisecond reads), the cache adds overhead with no benefit. Use it only when the underlying store has measurable read latency.

type CachedSnapshotStoreOptionsType = {
cache: Cache; // backing cache (required)
ttlMs?: number; // cache TTL in ms, default 5 minutes
keyPrefix?: string; // key prefix, default 'snap:'
};
FieldWhat
cacheBacking cache (required) — typically Redis in production. Any Cache: in-memory, Redis, or Memcached.
ttlMsCache TTL in milliseconds. Default: 5 minutes.
keyPrefixKey prefix (default 'snap:') — prevents collisions in shared caches.

The underlying store is the constructor’s first argumentnew CachedSnapshotStore(underlying, options) — not an option.

  • loadLatest(persistenceId) — read-through with TTL. Check cache; on hit, return. On miss, load from underlying, cache the result with ttlMs, return.
  • save(persistenceId, seq, state) — write-through-with-invalidate: delegate to the underlying store, then delete the cache entry. It deliberately does not write the new snapshot back — in a cluster two nodes may race on save, and a local write would leave a stale entry. The next read repopulates.
  • loadBefore(persistenceId, seq)not cached (too many possible seq values, and it’s used rarely — only during recovery).
  • delete(persistenceId, toSeq) — delegate to the underlying store, then invalidate the cache entry.

After a save, the cache entry is invalidated, so the next loadLatest re-fetches from the underlying store and returns the just-saved snapshot. TTL is the safety net: even if a node crashes between the underlying write and the cache invalidation, the stale entry expires within ttlMs.

For a slow underlying store (say, 50 ms per load), the cache turns subsequent loads of the same snapshot into sub-microsecond operations. A typical sharded-entity workload sees 80-95 % hit rate after warm-up.

The cache can be shared across nodes — back it with Redis or Memcached and every node reads from the same cache, so a snapshot loaded on one node is a hit on all the others. Back it with an in-memory cache instead and each node keeps its own copy; misses on a fresh node pay the full underlying-load cost.