Configuration
Plugin Options
| Option | Type | Default | Description |
|---|---|---|---|
client | string | 'default' | Named Redis client to use |
keyPrefix | string | '_sem:' | Prefix for semaphore keys (must be non-empty) |
defaultTtl | number | 30000 | Permit lease in ms - a crashed holder frees its seat after this |
maxTtl | number | 300000 | Hard cap for defaultTtl and per-call ttl |
retry.maxRetries | number | 3 | Retries after the initial acquire attempt |
retry.initialDelay | number | 100 | First backoff delay in ms |
retry.maxDelay | number | 3000 | Backoff ceiling in ms |
retry.multiplier | number | 2 | Exponential backoff multiplier (≥ 1) |
autoRenew.enabled | boolean | true | Renew held permits automatically |
autoRenew.intervalFraction | number | 0.5 | Renewal interval as a fraction of the TTL (strictly between 0 and 1) |
All options are validated fail-fast at registration (and again in the service constructor for hand-wired setups): non-positive timings, defaultTtl > maxTtl, an interval fraction outside (0, 1), or an empty keyPrefix throw InvalidSemaphoreConfigError before the application starts.
Synchronous Setup
import { Module } from '@nestjs/common';
import { RedisModule } from '@nestjs-redisx/core';
import { SemaphorePlugin } from '@nestjs-redisx/semaphore';
@Module({
imports: [
RedisModule.forRoot({
clients: {
host: 'localhost',
port: 6379,
},
plugins: [
new SemaphorePlugin({
defaultTtl: 30_000, // permit lease; a crashed holder frees its seat after this
maxTtl: 300_000, // hard cap for per-call ttl values
retry: { maxRetries: 3, initialDelay: 100, maxDelay: 3_000, multiplier: 2 },
autoRenew: { enabled: true, intervalFraction: 0.5 },
}),
],
}),
],
})
export class AppModule {}Async Registration
Load configuration through NestJS DI (ConfigService, Vault, …):
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { RedisModule } from '@nestjs-redisx/core';
import { SemaphorePlugin } from '@nestjs-redisx/semaphore';
@Module({
imports: [
RedisModule.forRoot({
clients: { host: 'localhost', port: 6379 },
plugins: [
SemaphorePlugin.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
defaultTtl: config.get<number>('SEMAPHORE_TTL_MS') ?? 30_000,
keyPrefix: config.get<string>('SEMAPHORE_PREFIX') ?? '_sem:',
}),
}),
],
}),
],
})
export class AppModule {}Per-Call Options
Every acquisition can override the plugin defaults:
| Option | Description |
|---|---|
ttl | Permit TTL for this acquisition (capped at maxTtl) |
waitTimeout | Total wait budget in ms - stop retrying once sleeping again would exceed it |
autoRenew | Enable/disable renewal for this permit |
retry.maxRetries, retry.initialDelay | Per-call retry overrides |
Choosing the TTL
The TTL is the crash-recovery bound: a holder that dies without releasing blocks its seat for at most the remaining TTL. With auto-renewal on (default), pick a TTL comfortably above one renewal round-trip (seconds, not milliseconds) and let renewal carry long work. With auto-renewal off, the TTL must exceed your worst-case execution time.
Capacity Discipline
permits is passed per call and must be the same for every caller of a key. The store enforces the capacity the acquiring call provides - two callers using different numbers for one key will observe the larger caller's view win whenever it acquires. Define the capacity once next to the key:
export const RENDER_SEMAPHORE = { key: 'reports:render', permits: 3 } as const;