Skip to content

feat(caching): add Redis Sentinel/Cluster support for HA session and cache layer#1073

Open
Ajibose wants to merge 4 commits into
rinafcode:mainfrom
Ajibose:feat/redis-sentinel-cluster-support
Open

feat(caching): add Redis Sentinel/Cluster support for HA session and cache layer#1073
Ajibose wants to merge 4 commits into
rinafcode:mainfrom
Ajibose:feat/redis-sentinel-cluster-support

Conversation

@Ajibose

@Ajibose Ajibose commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Closes #837

Summary

All Redis-backed services (SessionService, CachingService, ThreatDetectionService) previously connected to a single standalone Redis instance, and a couple of other services opened their own ad-hoc connections directly from process.env.REDIS_URL. A Redis node failure meant a full loss of session management and caching. This PR adds Redis Sentinel (HA failover) and Redis Cluster (sharding) support behind a single shared connection, so a node failure/failover no longer requires an application restart.

What changed

New files

  • src/common/redis/redis.module.ts — shared NestJS dynamic module (RedisModule.forRoot()) providing a single ioredis connection behind the REDIS_CLIENT DI token. @Global(), safe to import from multiple feature modules (NestJS de-dupes identical dynamic module registrations, and the underlying client is itself a process-wide singleton).
  • src/common/redis/redis.constants.tsREDIS_CLIENT token.
  • src/common/redis/redis.module.spec.ts — DI wiring tests (ioredis-mock).
  • src/config/cache.config.spec.ts — unit tests for the topology-selection factory (ioredis-mock).
  • src/session/session.module.spec.ts — integration test proving SessionModule serves real session read/writes through the shared connection, including under a Sentinel configuration.
  • src/media/validation/upload-progress.service.spec.ts — new unit tests (this service previously had none).

Modified files

  • src/config/cache.config.ts — the ioredis connection factory (RedisClientManager / getSharedRedisClient) now parses REDIS_CLUSTER_NODES / REDIS_SENTINEL_HOSTS (+ REDIS_SENTINEL_NAME, REDIS_SENTINEL_PASSWORD, REDIS_PASSWORD) and builds a Cluster, Sentinel-monitored, or standalone client accordingly (cluster takes precedence if both are set). Adds getRedisDeploymentMode() and a test-only resetSharedRedisClientForTests() singleton-reset helper.
  • src/session/session.module.tsSESSION_REDIS_CLIENT now aliases (useExisting) the shared REDIS_CLIENT from RedisModule.forRoot() instead of building its own client via a factory.
  • src/caching/caching.module.ts — the cache-manager Redis store now wraps the shared REDIS_CLIENT via cache-manager-ioredis-yet's redisInsStore() instead of opening a second, independent host/port connection. This collapses two Redis connections into one and gives the cache layer HA/sharding for free.
  • src/security/security.module.tsTHREAT_REDIS_CLIENT now aliases the shared REDIS_CLIENT the same way SessionModule does.
  • src/orchestration/locks/distributed-lock.service.ts — removed private redis = new Redis(process.env.REDIS_URL); now injects REDIS_CLIENT via the constructor.
  • src/media/validation/upload-progress.service.ts — removed new Redis(process.env.REDIS_URL || 'redis://localhost:6379'); now injects REDIS_CLIENT via the constructor.
  • src/config/env.validation.ts — documents the new optional env vars (REDIS_PASSWORD, REDIS_SENTINEL_HOSTS, REDIS_SENTINEL_NAME, REDIS_SENTINEL_PASSWORD, REDIS_CLUSTER_NODES).
  • .env.example — documents the same vars with usage notes.
  • test/utils/mock-factories.ts — added the missing setex command to the shared createMockRedisClient() factory (needed by UploadProgressService's tests).
  • src/orchestration/locks/distributed-lock.service.spec.ts — updated from a raw jest.mock('ioredis') constructor mock to the standard DI-token override pattern used elsewhere in the codebase.
  • package.json / package-lock.json — added ioredis-mock and @types/ioredis-mock as devDependencies.

Implementation details

  • Env vars (all optional; unset → standalone REDIS_HOST/REDIS_PORT, unchanged default behavior):
    • REDIS_CLUSTER_NODES — comma-separated host:port seed node list. Takes precedence over Sentinel if both are set.
    • REDIS_SENTINEL_HOSTS — comma-separated host:port sentinel list.
    • REDIS_SENTINEL_NAME — sentinel master group name (default mymaster).
    • REDIS_SENTINEL_PASSWORD — auth for the sentinel nodes themselves.
    • REDIS_PASSWORD — auth for the target Redis/Cluster nodes (used in all three modes).
  • Failover is handled internally by ioredis's Sentinel connector (re-resolves the current master) and Cluster client (refreshes slot ownership on MOVED/ASK), so no application restart is required when a replica is promoted.
  • getSharedRedisClient() keeps its declared Redis return type for source compatibility with existing call sites even when Cluster is selected internally, since Cluster implements the same command surface (get/set/multi/scan/eval/zadd/duplicate/...) every consumer already relies on — this keeps the diff scoped to the modules named in the issue instead of rippling a Redis | Cluster type change through every Redis-consuming file in the codebase. This tradeoff and its rationale are documented inline in cache.config.ts.
  • RedisModule is @Global() and marked safe to import repeatedly: NestJS de-duplicates dynamic module registrations with identical shape, and getSharedRedisClient is backed by a process-wide singleton regardless, so only one physical connection is ever opened.
  • DistributedLockService and UploadProgressService were not previously wired into any NestJS module (no providers: [...] referenced them), so converting them to constructor injection doesn't change any existing behavior — it just means they'll pick up Sentinel/Cluster support automatically once/if a module provides them.

Tests added

  • src/config/cache.config.spec.tsgetRedisDeploymentMode() mode-selection logic (standalone/sentinel/cluster/precedence), plus getSharedRedisClient() construction + working get/set roundtrip for all three topologies against ioredis-mock, plus singleton reuse/reset behavior.
  • src/common/redis/redis.module.spec.tsRedisModule.forRoot() provides a working REDIS_CLIENT, and repeated imports don't break DI resolution.
  • src/session/session.module.spec.ts — boots the real SessionModule and proves SESSION_REDIS_CLIENT is the same instance as REDIS_CLIENT, and that session create/read works end-to-end both in standalone mode and with REDIS_SENTINEL_HOSTS configured.
  • src/orchestration/locks/distributed-lock.service.spec.ts — updated to the DI-token mock pattern; same coverage as before (lock acquisition, contention, release, withLock).
  • src/media/validation/upload-progress.service.spec.ts — new coverage for initialize/update/complete/delete/list/statistics using the shared createMockRedisClient() factory.

All new/updated Redis-dependent tests are backed by ioredis-mock, so no real Redis/Sentinel/Cluster deployment is required to run them.

How to test

npm install
npm run lint:ci
npm run typecheck
npx jest src/config/cache.config.spec.ts src/common/redis/redis.module.spec.ts src/session/session.module.spec.ts src/orchestration/locks/distributed-lock.service.spec.ts src/media/validation/upload-progress.service.spec.ts

To exercise a real Sentinel/Cluster locally, set REDIS_SENTINEL_HOSTS/REDIS_SENTINEL_NAME or REDIS_CLUSTER_NODES in .env before starting the app — the shared connection will pick up the corresponding topology automatically with no code changes required.

…cache layer (rinafcode#837)

Add a shared RedisModule (src/common/redis) providing a single ioredis
connection to SessionModule, CachingModule, and SecurityModule, replacing
per-module connections and inline `new Redis(process.env.REDIS_URL)` calls
in DistributedLockService and UploadProgressService.

The underlying connection factory in cache.config.ts now parses
REDIS_SENTINEL_HOSTS/REDIS_SENTINEL_NAME and REDIS_CLUSTER_NODES to build
a Sentinel-monitored or Cluster connection instead of a plain standalone
client, so a Redis node failure no longer requires an application restart.
CachingModule's cache-manager store now wraps the shared connection via
redisInsStore instead of opening its own host/port connection.

Adds ioredis-mock-backed unit and integration tests covering standalone,
Sentinel, and Cluster topology selection and DI wiring.
@drips-wave

drips-wave Bot commented Jul 25, 2026

Copy link
Copy Markdown

@Ajibose Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@RUKAYAT-CODER

Copy link
Copy Markdown
Contributor

Well done on the job done so far!
Kindly resolve conflict.

Ajibose added 3 commits July 25, 2026 15:15
…m lock service

GitHub's web merge-conflict resolution on PR rinafcode#1073 stripped the trailing
newline from both files (failing eslint-plugin-prettier in CI) and left
behind an unused DISTRIBUTED_LOCK_REDIS constant from the conflicting side.
…IENT

DistributedLockService injects REDIS_CLIENT, not DISTRIBUTED_LOCK_REDIS,
so the module's own factory-based provider was dead wiring left over from
before the rinafcode#837 shared-Redis refactor. Removing the DISTRIBUTED_LOCK_REDIS
constant in a prior commit exposed this via TS2305 in CI typecheck.
Import RedisModule.forRoot() instead, matching session/caching/security
modules.
@RUKAYAT-CODER

Copy link
Copy Markdown
Contributor

Well done on the job done so far!
Kindly resolve conflict

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add Redis Sentinel/Cluster support for high-availability session and cache layer

2 participants