Redis
The usual first step out of one process. Membership, the distributed actor
directory and actor storage — all on one ioredis client.
Installation
pnpm add @sigx/actors-redis ioredisRequires Redis ≥ 7 — the directory claim uses SET NX GET. ioredis ≥ 5 is a peer
dependency.
What it provides
import { defineActorApp } from '@sigx/actors/host';
import { cluster } from '@sigx/actors/cluster';
import { redisCluster, redisStorage } from '@sigx/actors-redis';
export const app = defineActorApp({
actors,
storage: redisStorage({ url: process.env.REDIS_URL }),
}).use(cluster({
providers: redisCluster({ url: process.env.REDIS_URL }),
advertise: 'http://10.0.4.7:7311',
secret: process.env.HOST_SECRET,
}));
| Export | Role |
|---|---|
redisCluster() | membership + directory together |
redisMembership(client, opts) | membership alone |
redisDirectory(client, opts) | directory alone — pairs with k8s membership |
redisStorage() | etag-CAS ActorStorage |
redisReminders() | durable reminders on a due-time index |
Pass an existing client instead of url to share the app's connection —
redisCluster({ client }). Sharing one ioredis client across redisCluster() and
redisStorage() is safe and saves connections.
A client the package builds from url has enableAutoPipelining: true: ioredis coalesces the
commands issued in one event-loop tick into fewer socket writes, which is where a host busy
with many same-tick CAS saves spends its time. A client you pass keeps your own options — set
enableAutoPipelining: true on it yourself to get the same. The membership subscriber is
unaffected: it only ever subscribes, and ioredis never auto-pipelines subscription commands.
Call
redisCluster()once per host. Each call returns that host's own membership handle. The directory is safely shared.
Fencing
A host that cannot heartbeat past ttlMs self-fences: it stops claiming actors and
deactivates what it holds. Directory entries belonging to dead hosts are evicted lazily on
lookup.
State integrity never rests on any of this. The runtime's storage etag CAS is the floor — a briefly wrong directory entry costs a rejected save and a fault-and-reload, never corruption.
Reminders
Without a reminders: option, reminders ride redisStorage through the runtime's default
shardedReminders() — sixteen records that every set and every tick rewrite or scan. That is
right for hundreds of sleeping actors. For thousands, pass redisReminders():
import { redisReminders, redisStorage } from '@sigx/actors-redis';
export const app = defineActorApp({
actors,
storage: redisStorage({ client }),
reminders: redisReminders({ client }),
defaults: { reminderTickMs: 5_000 },
});
It keeps reminders on a sorted set scored by due time, so an arm is O(log N) and a tick
O(due) — measured flat to a million entries, where the sharded default spent 871 ms per set
with 100 000 asleep under load. An empty tick is one round trip, which is why
reminderTickMs can drop to a few seconds here and a durable wake lands within a couple of
seconds rather than within fifteen.
- One client for everything. Share it with
redisStorageandredisCluster. - Every host ticks the one set. Each claim is one Lua script that runs alone, so hosts get
disjoint members with no shard ownership — the same posture as
pgReminders. - Same delivery contract as every provider: at-most-once per firing, the server's clock
throughout, no catch-up burst after downtime, and a failed dispatch re-armed one tick out and
counted in
HostStats.remindersUndelivered. See the reminder contract and choosing a provider.
Not for Redis Cluster yet. The three reminder keys carry no hash tag, so they are not guaranteed to share a slot — the same as
redisStorage's keys.
Next steps
- Installation — setup and the options table.
- API reference — exports and the key layout.
- Clustering — where the providers plug in.
