@sigx/actors-redis · Preview

Redis#

The usual first step out of one process. Membership, the distributed actor directory and actor storage — all on one ioredis client.

v0.11.0 MIT

Installation#

Terminal
pnpm add @sigx/actors-redis ioredis

Requires Redis ≥ 7 — the directory claim uses SET NX GET. ioredis ≥ 5 is a peer dependency.

What it provides#

TypeScript
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,
}));
ExportRole
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():

TypeScript
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 redisStorage and redisCluster.
  • 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#