@beignet/provider-locks-redis
v0.0.52
Published
Redis-backed lock and lease provider for Beignet
Maintainers
Readme
@beignet/provider-locks-redis
Runtime: Beignet requires Node.js 22.12 or newer. Bun is optional.
Redis-backed LocksPort provider for Beignet applications.
The provider installs ctx.ports.locks using ioredis and Redis lease
semantics:
- acquire the lease and fencing token atomically with Redis Lua
- renew only when the stored owner token matches
- release only when the stored owner token matches
- optional fencing tokens from Redis
INCRinside the acquire script
createRedisLocksProvider(...) returns the stable RedisLocksProvider type.
RedisLocksConfig describes its validated config; the Zod schema remains
internal.
Install
bun add @beignet/provider-locks-redis @beignet/core ioredisRegister
// server/providers.ts
import { createRedisLocksProvider } from "@beignet/provider-locks-redis";
export const providers = [
createRedisLocksProvider({
prefix: "my-app:locks",
}),
];Set REDIS_LOCKS_URL for the default env-backed provider. You can also pass an
existing Redis-compatible client to createRedisLocksProvider({ client }).
Environment variables:
REDIS_LOCKS_URL(required unless you passclient)REDIS_LOCKS_DBREDIS_LOCKS_PREFIXREDIS_LOCKS_CONNECT_TIMEOUT_MSREDIS_LOCKS_SHUTDOWN_TIMEOUT_MSREDIS_LOCKS_MAX_RETRIES_PER_REQUESTREDIS_LOCKS_CONNECT_MAX_ATTEMPTS
Environment-backed numeric values must be non-negative integer strings. Pass
numbers to the matching db, connectTimeoutMs, shutdownTimeoutMs, and
maxRetriesPerRequest factory options. Both forms require JavaScript safe
integers. Connection and shutdown timeouts cannot exceed 2,147,483,647
milliseconds, the runtime timer ceiling; shutdown timeouts must be positive.
Lease waitMs must be an integer from 0 through that ceiling;
retryDelayMs must be an integer from 1 through that ceiling.
beignet doctor --strict checks that installed Redis locks providers are
registered in server/providers.ts and that REDIS_LOCKS_URL is present in
app env examples or config when the env-backed provider is used.
Lease owner tokens use Web Crypto randomUUID() or getRandomValues() when
available and securely fall back to node:crypto on supported Node runtimes.
The direct createRedisLocks(...) adapter accepts createOwnerToken for
deterministic tests; production wiring should keep the secure runtime default.
Use
const result = await ctx.ports.locks.acquire("schedule:daily-report", {
ttlMs: 60_000,
waitMs: 0,
});
if (!result.acquired) return;
try {
await runDailyReport(ctx);
} finally {
await result.lease.release();
}Or use withLease(...):
await ctx.ports.locks.withLease(
"outbox:drain",
{ ttlMs: 30_000, waitMs: 5_000 },
async ({ lease }) => {
await drainOutbox(ctx, { fencingToken: lease.fencingToken });
},
);Restore a handle in a later invocation with the persisted owner token and an explicit renewal TTL:
const lease = ctx.ports.locks.restore(key, ownerToken, {
ttlMs: 60_000,
expiresAt: persistedExpiresAt,
fencingToken: persistedFencingToken,
});Omit expiry or fencing metadata when it was not persisted; the adapter does not invent either value. Redis still verifies the owner token atomically on renew and release.
API
createRedisLocks(options)
Creates a LocksPort from a Redis-compatible client. Use this for tests or
custom provider composition.
createRedisLocksProvider(options)
Creates a Beignet lifecycle provider that contributes:
ctx.ports.locks, the standard BeignetLocksPortctx.ports.redisLocks, an escape hatch with the raw Redis client, prefix, andcheckHealth()helper
createRedisLocksProvider()
Ready-to-register provider using REDIS_LOCKS_* environment variables.
Devtools
When @beignet/devtools or another provider instrumentation sink is installed
before this provider, lock acquire, renew, release, and skipped-acquire
activity appears under the Locks watcher. Instrumentation includes the lock key,
lease timing, acquisition status, and duration; owner tokens and result payloads
are not recorded.
Failure behavior
The env-backed provider throws during startup when REDIS_LOCKS_URL is missing
or Redis cannot be reached within the configured connection attempts. Runtime
lock operations throw Redis errors so callers can fail or retry explicitly.
Provider-owned clients get REDIS_LOCKS_SHUTDOWN_TIMEOUT_MS milliseconds
(default 5000) for a graceful QUIT. A rejection or timeout forces a
disconnect and rejects server.stop(), so process entrypoints can report an
incomplete shutdown.
withLease(...) returns a skipped result when the lease cannot be acquired; do
not treat a skipped lease as a successful undefined result.
Use ctx.ports.redisLocks.checkHealth() from app-owned readiness endpoints to
verify that Redis can run a cheap PING or no-op script without starting any
background lock work.
Local and tests
Use app-owned fake locks or an in-memory LocksPort in use-case tests when
lease behavior is not the subject of the test. Use createRedisLocks(...) with
a test Redis client for provider-level tests that need Lua/fencing semantics.
This package also includes an opt-in live Redis suite. It uses independent
Redis connections to exercise concurrent acquisition, monotonic fencing,
expired-owner rejection, bounded waiting, and timeouts. The default
bun run test command remains hermetic. Set REDIS_LOCKS_TEST_URL or the
shared REDIS_TEST_URL for the live suite:
REDIS_LOCKS_TEST_URL=redis://localhost:6379 bun run test:liveDeployment notes
Use a dedicated single Redis primary with persistence, predictable latency, and
maxmemory-policy noeviction for work that depends on fencing tokens. The
fencing counter has no TTL; an allkeys-* eviction policy can delete it and
allow a later INCR to restart at 1. Beignet supports noeviction as the
production policy so memory pressure fails the lock operation instead of
silently reusing a fencing token. Verify the policy in the Redis deployment
configuration rather than relying on application startup access to CONFIG.
The provider's two-key Lua acquisition does not currently support Redis Cluster. Primary failover with asynchronous replication can lose recent lease or fencing-counter writes, and the live suite does not simulate failover or network partitions.
Fencing tokens become a correctness boundary only when the protected durable resource atomically accepts tokens strictly greater than the last token it observed. Without that downstream check, use the lease to reduce duplicate work, not to prove that duplicates are impossible.
Correctness note
Locks coordinate work; they are not a substitute for durable correctness. Use database unique constraints, transactions, idempotency keys, and outbox claims as the source of truth for business invariants. Use leases to prevent duplicate scheduler runs, singleton workers, cache stampedes, and short critical sections.
