mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
fix(redis): apply TLS SNI override to pub/sub clients (#4638)
* fix(redis): apply TLS SNI override to pub/sub clients too Pub/sub clients in lib/events/pubsub.ts build their own ioredis instances directly via new Redis(redisUrl, ...) because pub/sub needs dedicated connections (can't multiplex on the shared client from getRedisClient). That path skipped the resolveTlsOptions helper added for trigger.dev's PrivateLink VPCE IP, so every pub/sub channel hit 'Hostname/IP does not match certificate's altnames' on connect. Export the helper as resolveRedisTlsOptions and use it from pubsub.ts. * refactor(redis): share connection defaults via one helper Extract keepAlive/connectTimeout/enableOfflineQueue + TLS SNI into a single getRedisConnectionDefaults helper. Main client and pub/sub clients both spread it; caller-specific retry/timeout policy stays per-caller (pub/sub still needs maxRetriesPerRequest: null and a different retry strategy for SUBSCRIBE). * fix(pubsub): surface TLS config errors instead of silently degrading resolveRedisTlsOptions (via getRedisConnectionDefaults) throws if REDIS_TLS_SERVERNAME is missing for an IP-based rediss:// URL. Calling it inside the constructor let createPubSubChannel's try/catch swallow the error and fall back to in-process EventEmitter — silent cross-replica pub/sub breakage in prod. Resolve defaults before the try so config errors propagate; only catch genuine runtime construction failures.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import { randomFloat } from '@sim/utils/random'
|
||||
import Redis from 'ioredis'
|
||||
import Redis, { type RedisOptions } from 'ioredis'
|
||||
import { env } from '@/lib/core/config/env'
|
||||
|
||||
const logger = createLogger('Redis')
|
||||
@@ -16,7 +16,7 @@ const redisUrl = env.REDIS_URL
|
||||
*
|
||||
* For DNS hosts: no override needed, default verification works.
|
||||
*/
|
||||
function resolveTlsOptions(url: string | undefined): { servername: string } | undefined {
|
||||
function resolveRedisTlsOptions(url: string | undefined): { servername: string } | undefined {
|
||||
if (!url) return undefined
|
||||
let parsed: URL
|
||||
try {
|
||||
@@ -37,6 +37,23 @@ function resolveTlsOptions(url: string | undefined): { servername: string } | un
|
||||
return { servername: env.REDIS_TLS_SERVERNAME }
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared connection defaults — keepAlive, connectTimeout, enableOfflineQueue,
|
||||
* and TLS SNI when REDIS_URL targets an IP. Every Redis client we open should
|
||||
* spread this; callers add their own retry / timeout policy on top.
|
||||
*/
|
||||
export function getRedisConnectionDefaults(
|
||||
url: string | undefined
|
||||
): Pick<RedisOptions, 'keepAlive' | 'connectTimeout' | 'enableOfflineQueue' | 'tls'> {
|
||||
const tls = resolveRedisTlsOptions(url)
|
||||
return {
|
||||
keepAlive: 1000,
|
||||
connectTimeout: 10000,
|
||||
enableOfflineQueue: true,
|
||||
...(tls ? { tls } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
let globalRedisClient: Redis | null = null
|
||||
let pingFailures = 0
|
||||
let pingInterval: NodeJS.Timeout | null = null
|
||||
@@ -117,18 +134,15 @@ export function getRedisClient(): Redis | null {
|
||||
if (globalRedisClient) return globalRedisClient
|
||||
|
||||
// Outside the try/catch so config errors aren't silently swallowed.
|
||||
const tls = resolveTlsOptions(redisUrl)
|
||||
const defaults = getRedisConnectionDefaults(redisUrl)
|
||||
|
||||
try {
|
||||
logger.info('Initializing Redis client')
|
||||
|
||||
globalRedisClient = new Redis(redisUrl, {
|
||||
keepAlive: 1000,
|
||||
connectTimeout: 10000,
|
||||
...defaults,
|
||||
commandTimeout: 5000,
|
||||
maxRetriesPerRequest: 5,
|
||||
enableOfflineQueue: true,
|
||||
...(tls ? { tls } : {}),
|
||||
|
||||
retryStrategy: (times) => {
|
||||
if (times > 10) {
|
||||
|
||||
@@ -9,6 +9,7 @@ import { EventEmitter } from 'events'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import Redis, { type RedisOptions } from 'ioredis'
|
||||
import { env } from '@/lib/core/config/env'
|
||||
import { getRedisConnectionDefaults } from '@/lib/core/config/redis'
|
||||
|
||||
const logger = createLogger('PubSub')
|
||||
|
||||
@@ -31,13 +32,12 @@ class RedisPubSubChannel<T> implements PubSubChannel<T> {
|
||||
|
||||
constructor(
|
||||
redisUrl: string,
|
||||
connectionDefaults: ReturnType<typeof getRedisConnectionDefaults>,
|
||||
private config: PubSubChannelConfig
|
||||
) {
|
||||
const commonOpts = {
|
||||
keepAlive: 1000,
|
||||
connectTimeout: 10000,
|
||||
...connectionDefaults,
|
||||
maxRetriesPerRequest: null,
|
||||
enableOfflineQueue: true,
|
||||
retryStrategy: (times: number) => {
|
||||
if (times > 10) return 30000
|
||||
return Math.min(times * 500, 5000)
|
||||
@@ -139,16 +139,18 @@ class LocalPubSubChannel<T> implements PubSubChannel<T> {
|
||||
|
||||
export function createPubSubChannel<T>(config: PubSubChannelConfig): PubSubChannel<T> {
|
||||
const redisUrl = env.REDIS_URL
|
||||
if (!redisUrl) return new LocalPubSubChannel<T>(config)
|
||||
|
||||
if (redisUrl) {
|
||||
try {
|
||||
logger.info(`${config.label}: Using Redis`)
|
||||
return new RedisPubSubChannel<T>(redisUrl, config)
|
||||
} catch (err) {
|
||||
logger.error(`Failed to create Redis ${config.label}, falling back to local:`, err)
|
||||
return new LocalPubSubChannel<T>(config)
|
||||
}
|
||||
// Resolve config-derived defaults outside the try so a missing
|
||||
// REDIS_TLS_SERVERNAME (config error) surfaces instead of silently degrading
|
||||
// to the in-process EventEmitter — that would break cross-replica pub/sub.
|
||||
const connectionDefaults = getRedisConnectionDefaults(redisUrl)
|
||||
|
||||
try {
|
||||
logger.info(`${config.label}: Using Redis`)
|
||||
return new RedisPubSubChannel<T>(redisUrl, connectionDefaults, config)
|
||||
} catch (err) {
|
||||
logger.error(`Failed to create Redis ${config.label}, falling back to local:`, err)
|
||||
return new LocalPubSubChannel<T>(config)
|
||||
}
|
||||
|
||||
return new LocalPubSubChannel<T>(config)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user