feat(caching): add Redis Sentinel/Cluster support for HA session and cache layer#1073
Open
Ajibose wants to merge 4 commits into
Open
feat(caching): add Redis Sentinel/Cluster support for HA session and cache layer#1073Ajibose wants to merge 4 commits into
Ajibose wants to merge 4 commits into
Conversation
…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.
|
@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! 🚀 |
Contributor
|
Well done on the job done so far! |
…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.
Contributor
|
Well done on the job done so far! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 fromprocess.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 theREDIS_CLIENTDI 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.ts—REDIS_CLIENTtoken.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 provingSessionModuleserves 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 parsesREDIS_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). AddsgetRedisDeploymentMode()and a test-onlyresetSharedRedisClientForTests()singleton-reset helper.src/session/session.module.ts—SESSION_REDIS_CLIENTnow aliases (useExisting) the sharedREDIS_CLIENTfromRedisModule.forRoot()instead of building its own client via a factory.src/caching/caching.module.ts— thecache-managerRedis store now wraps the sharedREDIS_CLIENTviacache-manager-ioredis-yet'sredisInsStore()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.ts—THREAT_REDIS_CLIENTnow aliases the sharedREDIS_CLIENTthe same waySessionModuledoes.src/orchestration/locks/distributed-lock.service.ts— removedprivate redis = new Redis(process.env.REDIS_URL); now injectsREDIS_CLIENTvia the constructor.src/media/validation/upload-progress.service.ts— removednew Redis(process.env.REDIS_URL || 'redis://localhost:6379'); now injectsREDIS_CLIENTvia 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 missingsetexcommand to the sharedcreateMockRedisClient()factory (needed byUploadProgressService's tests).src/orchestration/locks/distributed-lock.service.spec.ts— updated from a rawjest.mock('ioredis')constructor mock to the standard DI-token override pattern used elsewhere in the codebase.package.json/package-lock.json— addedioredis-mockand@types/ioredis-mockas devDependencies.Implementation details
REDIS_HOST/REDIS_PORT, unchanged default behavior):REDIS_CLUSTER_NODES— comma-separatedhost:portseed node list. Takes precedence over Sentinel if both are set.REDIS_SENTINEL_HOSTS— comma-separatedhost:portsentinel list.REDIS_SENTINEL_NAME— sentinel master group name (defaultmymaster).REDIS_SENTINEL_PASSWORD— auth for the sentinel nodes themselves.REDIS_PASSWORD— auth for the target Redis/Cluster nodes (used in all three modes).ioredis's Sentinel connector (re-resolves the current master) and Cluster client (refreshes slot ownership onMOVED/ASK), so no application restart is required when a replica is promoted.getSharedRedisClient()keeps its declaredRedisreturn type for source compatibility with existing call sites even when Cluster is selected internally, sinceClusterimplements 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 aRedis | Clustertype change through every Redis-consuming file in the codebase. This tradeoff and its rationale are documented inline incache.config.ts.RedisModuleis@Global()and marked safe to import repeatedly: NestJS de-duplicates dynamic module registrations with identical shape, andgetSharedRedisClientis backed by a process-wide singleton regardless, so only one physical connection is ever opened.DistributedLockServiceandUploadProgressServicewere not previously wired into any NestJS module (noproviders: [...]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.ts—getRedisDeploymentMode()mode-selection logic (standalone/sentinel/cluster/precedence), plusgetSharedRedisClient()construction + working get/set roundtrip for all three topologies againstioredis-mock, plus singleton reuse/reset behavior.src/common/redis/redis.module.spec.ts—RedisModule.forRoot()provides a workingREDIS_CLIENT, and repeated imports don't break DI resolution.src/session/session.module.spec.ts— boots the realSessionModuleand provesSESSION_REDIS_CLIENTis the same instance asREDIS_CLIENT, and that session create/read works end-to-end both in standalone mode and withREDIS_SENTINEL_HOSTSconfigured.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 sharedcreateMockRedisClient()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
To exercise a real Sentinel/Cluster locally, set
REDIS_SENTINEL_HOSTS/REDIS_SENTINEL_NAMEorREDIS_CLUSTER_NODESin.envbefore starting the app — the shared connection will pick up the corresponding topology automatically with no code changes required.