Skip to content

Commit 4945d55

Browse files
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.
1 parent bd9e692 commit 4945d55

2 files changed

Lines changed: 37 additions & 21 deletions

File tree

apps/sim/lib/core/config/redis.ts

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { createLogger } from '@sim/logger'
22
import { toError } from '@sim/utils/errors'
33
import { randomFloat } from '@sim/utils/random'
4-
import Redis from 'ioredis'
4+
import Redis, { type RedisOptions } from 'ioredis'
55
import { env } from '@/lib/core/config/env'
66

77
const logger = createLogger('Redis')
@@ -16,7 +16,7 @@ const redisUrl = env.REDIS_URL
1616
*
1717
* For DNS hosts: no override needed, default verification works.
1818
*/
19-
function resolveTlsOptions(url: string | undefined): { servername: string } | undefined {
19+
function resolveRedisTlsOptions(url: string | undefined): { servername: string } | undefined {
2020
if (!url) return undefined
2121
let parsed: URL
2222
try {
@@ -37,6 +37,23 @@ function resolveTlsOptions(url: string | undefined): { servername: string } | un
3737
return { servername: env.REDIS_TLS_SERVERNAME }
3838
}
3939

40+
/**
41+
* Shared connection defaults — keepAlive, connectTimeout, enableOfflineQueue,
42+
* and TLS SNI when REDIS_URL targets an IP. Every Redis client we open should
43+
* spread this; callers add their own retry / timeout policy on top.
44+
*/
45+
export function getRedisConnectionDefaults(
46+
url: string | undefined
47+
): Pick<RedisOptions, 'keepAlive' | 'connectTimeout' | 'enableOfflineQueue' | 'tls'> {
48+
const tls = resolveRedisTlsOptions(url)
49+
return {
50+
keepAlive: 1000,
51+
connectTimeout: 10000,
52+
enableOfflineQueue: true,
53+
...(tls ? { tls } : {}),
54+
}
55+
}
56+
4057
let globalRedisClient: Redis | null = null
4158
let pingFailures = 0
4259
let pingInterval: NodeJS.Timeout | null = null
@@ -117,18 +134,15 @@ export function getRedisClient(): Redis | null {
117134
if (globalRedisClient) return globalRedisClient
118135

119136
// Outside the try/catch so config errors aren't silently swallowed.
120-
const tls = resolveTlsOptions(redisUrl)
137+
const defaults = getRedisConnectionDefaults(redisUrl)
121138

122139
try {
123140
logger.info('Initializing Redis client')
124141

125142
globalRedisClient = new Redis(redisUrl, {
126-
keepAlive: 1000,
127-
connectTimeout: 10000,
143+
...defaults,
128144
commandTimeout: 5000,
129145
maxRetriesPerRequest: 5,
130-
enableOfflineQueue: true,
131-
...(tls ? { tls } : {}),
132146

133147
retryStrategy: (times) => {
134148
if (times > 10) {

apps/sim/lib/events/pubsub.ts

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { EventEmitter } from 'events'
99
import { createLogger } from '@sim/logger'
1010
import Redis, { type RedisOptions } from 'ioredis'
1111
import { env } from '@/lib/core/config/env'
12+
import { getRedisConnectionDefaults } from '@/lib/core/config/redis'
1213

1314
const logger = createLogger('PubSub')
1415

@@ -31,13 +32,12 @@ class RedisPubSubChannel<T> implements PubSubChannel<T> {
3132

3233
constructor(
3334
redisUrl: string,
35+
connectionDefaults: ReturnType<typeof getRedisConnectionDefaults>,
3436
private config: PubSubChannelConfig
3537
) {
3638
const commonOpts = {
37-
keepAlive: 1000,
38-
connectTimeout: 10000,
39+
...connectionDefaults,
3940
maxRetriesPerRequest: null,
40-
enableOfflineQueue: true,
4141
retryStrategy: (times: number) => {
4242
if (times > 10) return 30000
4343
return Math.min(times * 500, 5000)
@@ -139,16 +139,18 @@ class LocalPubSubChannel<T> implements PubSubChannel<T> {
139139

140140
export function createPubSubChannel<T>(config: PubSubChannelConfig): PubSubChannel<T> {
141141
const redisUrl = env.REDIS_URL
142-
143-
if (redisUrl) {
144-
try {
145-
logger.info(`${config.label}: Using Redis`)
146-
return new RedisPubSubChannel<T>(redisUrl, config)
147-
} catch (err) {
148-
logger.error(`Failed to create Redis ${config.label}, falling back to local:`, err)
149-
return new LocalPubSubChannel<T>(config)
150-
}
142+
if (!redisUrl) return new LocalPubSubChannel<T>(config)
143+
144+
// Resolve config-derived defaults outside the try so a missing
145+
// REDIS_TLS_SERVERNAME (config error) surfaces instead of silently degrading
146+
// to the in-process EventEmitter — that would break cross-replica pub/sub.
147+
const connectionDefaults = getRedisConnectionDefaults(redisUrl)
148+
149+
try {
150+
logger.info(`${config.label}: Using Redis`)
151+
return new RedisPubSubChannel<T>(redisUrl, connectionDefaults, config)
152+
} catch (err) {
153+
logger.error(`Failed to create Redis ${config.label}, falling back to local:`, err)
154+
return new LocalPubSubChannel<T>(config)
151155
}
152-
153-
return new LocalPubSubChannel<T>(config)
154156
}

0 commit comments

Comments
 (0)