From b58fa66e0ea36b5766a1be3744093cc519b7ecdf Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Sat, 16 May 2026 20:22:30 -0700 Subject: [PATCH 1/3] 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. --- apps/sim/lib/core/config/redis.ts | 6 ++++-- apps/sim/lib/events/pubsub.ts | 4 ++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/apps/sim/lib/core/config/redis.ts b/apps/sim/lib/core/config/redis.ts index aea707daa00..b66eb36f893 100644 --- a/apps/sim/lib/core/config/redis.ts +++ b/apps/sim/lib/core/config/redis.ts @@ -16,7 +16,9 @@ const redisUrl = env.REDIS_URL * * For DNS hosts: no override needed, default verification works. */ -function resolveTlsOptions(url: string | undefined): { servername: string } | undefined { +export function resolveRedisTlsOptions( + url: string | undefined +): { servername: string } | undefined { if (!url) return undefined let parsed: URL try { @@ -117,7 +119,7 @@ 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 tls = resolveRedisTlsOptions(redisUrl) try { logger.info('Initializing Redis client') diff --git a/apps/sim/lib/events/pubsub.ts b/apps/sim/lib/events/pubsub.ts index b299eafc055..d557e9cd7d8 100644 --- a/apps/sim/lib/events/pubsub.ts +++ b/apps/sim/lib/events/pubsub.ts @@ -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 { resolveRedisTlsOptions } from '@/lib/core/config/redis' const logger = createLogger('PubSub') @@ -33,6 +34,8 @@ class RedisPubSubChannel implements PubSubChannel { redisUrl: string, private config: PubSubChannelConfig ) { + const tls = resolveRedisTlsOptions(redisUrl) + const commonOpts = { keepAlive: 1000, connectTimeout: 10000, @@ -42,6 +45,7 @@ class RedisPubSubChannel implements PubSubChannel { if (times > 10) return 30000 return Math.min(times * 500, 5000) }, + ...(tls ? { tls } : {}), } satisfies RedisOptions this.pub = new Redis(redisUrl, { ...commonOpts, connectionName: `${config.label}-pub` }) From e2d7e36f5d7020c9ebdb70308b2da6f06b9a3acc Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Sat, 16 May 2026 20:32:23 -0700 Subject: [PATCH 2/3] 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). --- apps/sim/lib/core/config/redis.ts | 30 +++++++++++++++++++++--------- apps/sim/lib/events/pubsub.ts | 9 ++------- 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/apps/sim/lib/core/config/redis.ts b/apps/sim/lib/core/config/redis.ts index b66eb36f893..04f976b44ae 100644 --- a/apps/sim/lib/core/config/redis.ts +++ b/apps/sim/lib/core/config/redis.ts @@ -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,9 +16,7 @@ const redisUrl = env.REDIS_URL * * For DNS hosts: no override needed, default verification works. */ -export function resolveRedisTlsOptions( - url: string | undefined -): { servername: string } | undefined { +function resolveRedisTlsOptions(url: string | undefined): { servername: string } | undefined { if (!url) return undefined let parsed: URL try { @@ -39,6 +37,23 @@ export function resolveRedisTlsOptions( 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 { + 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 @@ -119,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 = resolveRedisTlsOptions(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) { diff --git a/apps/sim/lib/events/pubsub.ts b/apps/sim/lib/events/pubsub.ts index d557e9cd7d8..8fe58729991 100644 --- a/apps/sim/lib/events/pubsub.ts +++ b/apps/sim/lib/events/pubsub.ts @@ -9,7 +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 { resolveRedisTlsOptions } from '@/lib/core/config/redis' +import { getRedisConnectionDefaults } from '@/lib/core/config/redis' const logger = createLogger('PubSub') @@ -34,18 +34,13 @@ class RedisPubSubChannel implements PubSubChannel { redisUrl: string, private config: PubSubChannelConfig ) { - const tls = resolveRedisTlsOptions(redisUrl) - const commonOpts = { - keepAlive: 1000, - connectTimeout: 10000, + ...getRedisConnectionDefaults(redisUrl), maxRetriesPerRequest: null, - enableOfflineQueue: true, retryStrategy: (times: number) => { if (times > 10) return 30000 return Math.min(times * 500, 5000) }, - ...(tls ? { tls } : {}), } satisfies RedisOptions this.pub = new Redis(redisUrl, { ...commonOpts, connectionName: `${config.label}-pub` }) From ba0a98333955367ec30b3e19090e951ce29e8a0f Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Sat, 16 May 2026 22:23:18 -0700 Subject: [PATCH 3/3] fix(pubsub): surface TLS config errors instead of silently degrading MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- apps/sim/lib/events/pubsub.ts | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/apps/sim/lib/events/pubsub.ts b/apps/sim/lib/events/pubsub.ts index 8fe58729991..f866d8d1459 100644 --- a/apps/sim/lib/events/pubsub.ts +++ b/apps/sim/lib/events/pubsub.ts @@ -32,10 +32,11 @@ class RedisPubSubChannel implements PubSubChannel { constructor( redisUrl: string, + connectionDefaults: ReturnType, private config: PubSubChannelConfig ) { const commonOpts = { - ...getRedisConnectionDefaults(redisUrl), + ...connectionDefaults, maxRetriesPerRequest: null, retryStrategy: (times: number) => { if (times > 10) return 30000 @@ -138,16 +139,18 @@ class LocalPubSubChannel implements PubSubChannel { export function createPubSubChannel(config: PubSubChannelConfig): PubSubChannel { const redisUrl = env.REDIS_URL - - if (redisUrl) { - try { - logger.info(`${config.label}: Using Redis`) - return new RedisPubSubChannel(redisUrl, config) - } catch (err) { - logger.error(`Failed to create Redis ${config.label}, falling back to local:`, err) - return new LocalPubSubChannel(config) - } + if (!redisUrl) return new LocalPubSubChannel(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(redisUrl, connectionDefaults, config) + } catch (err) { + logger.error(`Failed to create Redis ${config.label}, falling back to local:`, err) + return new LocalPubSubChannel(config) } - - return new LocalPubSubChannel(config) }