From d4d3dae8e7f654046595db3199c2f0caaad79333 Mon Sep 17 00:00:00 2001 From: Frankie Moran Date: Fri, 10 Jul 2026 16:39:43 +0100 Subject: [PATCH] =?UTF-8?q?Reapply=20"PO-8886=20Adding=20support=20for=20m?= =?UTF-8?q?anaged=20redis=20cluster=20sessions=20(#3=E2=80=A6=20(#356)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 56fc01651775befb83ba6948e25bad35e7592bc7. --- CHANGELOG.md | 1 + src/interfaces/session-storage-config.ts | 1 + src/session/session-storage/index.ts | 69 ++++++++++++++++++------ yarn-known-issues | 1 + 4 files changed, 57 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 01a0010..8adda29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ The format is based on Keep a Changelog and this project follows semantic versio - Remove the obsolete generic `opalApiUrl` proxy configuration from `ProxyConfiguration` and default proxy config. - Clear the logged-in user's Redis-backed user-state cache entry during SSO logout. +- Support a cluster-aware Redis session client for managed Redis deployments while retaining standalone Redis by default. ## Changelog Policy diff --git a/src/interfaces/session-storage-config.ts b/src/interfaces/session-storage-config.ts index 9422785..ee292e5 100644 --- a/src/interfaces/session-storage-config.ts +++ b/src/interfaces/session-storage-config.ts @@ -7,6 +7,7 @@ class SessionStorageConfiguration { domain!: string; redisEnabled!: boolean; redisConnectionString!: string; + redisClusterEnabled!: boolean; } export default SessionStorageConfiguration; diff --git a/src/session/session-storage/index.ts b/src/session/session-storage/index.ts index f0bbfc4..03ab1b1 100644 --- a/src/session/session-storage/index.ts +++ b/src/session/session-storage/index.ts @@ -4,35 +4,69 @@ import { RedisStore } from 'connect-redis'; import cookieParser from 'cookie-parser'; import { Application } from 'express'; import session from 'express-session'; -import { createClient } from 'redis'; +import { createClient, createCluster, type RedisClientType, type RedisClusterType } from 'redis'; import FileStoreFactory from 'session-file-store'; import { REDIS_CLIENT_APP_LOCAL_KEY } from '../../constants/redis-client-app-local-key.js'; const FileStore = FileStoreFactory(session); const logger = Logger.getLogger('session-storage'); +const MAX_REDIS_RECONNECT_ATTEMPTS = 20; +const REDIS_TLS_PROTOCOL = 'rediss:'; + +type RedisSessionClient = RedisClientType | RedisClusterType; export default class SessionStorage { - private getStore(app: Application, enabled: boolean, connectionString: string) { - if (enabled) { - const redisUrl = new URL(connectionString); - logger.info('Using Redis session store', `${redisUrl.protocol}//${redisUrl.host}`); - const client = createClient({ + private getReconnectStrategy() { + return function (retries: number) { + if (retries > MAX_REDIS_RECONNECT_ATTEMPTS) { + logger.log('Too many attempts to reconnect. Redis connection was terminated'); + return new Error('Too many retries.'); + } else { + return retries * 500; + } + }; + } + + private createRedisClient(connectionString: string, clusterEnabled: boolean): RedisSessionClient { + if (!clusterEnabled) { + return createClient({ url: connectionString, socket: { - reconnectStrategy: function (retries) { - if (retries > 20) { - logger.log('Too many attempts to reconnect. Redis connection was terminated'); - return new Error('Too many retries.'); - } else { - return retries * 500; - } - }, + reconnectStrategy: this.getReconnectStrategy(), }, }); + } + + const redisUrl = new URL(connectionString); + + return createCluster({ + rootNodes: [{ url: connectionString }], + defaults: { + ...(redisUrl.username ? { username: decodeURIComponent(redisUrl.username) } : {}), + ...(redisUrl.password ? { password: decodeURIComponent(redisUrl.password) } : {}), + socket: { + tls: redisUrl.protocol === REDIS_TLS_PROTOCOL, + reconnectStrategy: this.getReconnectStrategy(), + }, + }, + }); + } + + private getStore(app: Application, enabled: boolean, connectionString: string, clusterEnabled: boolean) { + if (enabled) { + const redisUrl = new URL(connectionString); + logger.info( + `Using ${clusterEnabled ? 'clustered' : 'standalone'} Redis session store`, + `${redisUrl.protocol}//${redisUrl.host}`, + ); + const client = this.createRedisClient(connectionString, clusterEnabled); client.on('error', (err) => { logger.error('Redis Client Error', err); }); + client.on('node-error', (err) => { + logger.error('Redis Cluster Node Error', err); + }); client.connect().catch(() => { process.exit(); @@ -62,7 +96,12 @@ export default class SessionStorage { domain: sessionStorage.domain, }, rolling: true, - store: this.getStore(app, sessionStorage.redisEnabled, sessionStorage.redisConnectionString), + store: this.getStore( + app, + sessionStorage.redisEnabled, + sessionStorage.redisConnectionString, + sessionStorage.redisClusterEnabled === true, + ), }), ); } diff --git a/yarn-known-issues b/yarn-known-issues index 64d7a38..fab463b 100644 --- a/yarn-known-issues +++ b/yarn-known-issues @@ -1 +1,2 @@ +{"value":"@opentelemetry/core","children":{"ID":1120821,"Issue":"OpenTelemetry Core: Unbounded memory allocation in W3C Baggage propagation","URL":"https://github.com/advisories/GHSA-8988-4f7v-96qf","Severity":"moderate","Vulnerable Versions":"<2.8.0","Tree Versions":["1.30.1","2.0.1"],"Dependents":["@azure/opentelemetry-instrumentation-azure-sdk@npm:1.0.0-beta.9","applicationinsights@virtual:5843eacae55b71dda0add913fd01c2f69e8548e48f97802ab3aa541175282495254dd2e854c37d72d02e8efecf55850dba3d1505e8b006284e02644e6f897e9c#npm:2.9.8"]}} {"value":"uuid","children":{"ID":1119441,"Issue":"uuid: Missing buffer bounds check in v3/v5/v6 when buf is provided","URL":"https://github.com/advisories/GHSA-w5hq-g745-h8pq","Severity":"moderate","Vulnerable Versions":"<11.1.1","Tree Versions":["8.3.2"],"Dependents":["@azure/msal-node@npm:3.8.6"]}}