From a0f4c86a74625c14d73c96c422324570a8f13dc8 Mon Sep 17 00:00:00 2001 From: claude code agent 227 Date: Mon, 29 Jun 2026 04:28:56 +0300 Subject: [PATCH 1/2] fix(ws): disconnect socket.io redis adapter pub/sub clients on shutdown The WsRedisIoAdapter creates two ioredis clients (pubClient/subClient) for @socket.io/redis-adapter but never closed them, leaking their TCP handles on application shutdown (#255). The redis-adapter does not own these clients' lifecycle, and the adapter is instantiated from main.ts (not a DI provider), so no Nest lifecycle hook applied to it. Keep references to both clients and override dispose(), which Nest's SocketModule.close() invokes exactly once during shutdown after all socket.io servers are closed. Use disconnect(false) to mirror the sibling pub/sub pair in collaboration/extensions/redis-sync (onDestroy): immediate close, no QUIT round-trip, no auto-reconnect. Refs are nulled to guard against double-close. Runtime behavior is unchanged; only the shutdown path is added. Verified with a script that boots connectToRedis() against a real Redis: 2 sockets to :6379 open after connect, 0 remain after dispose(). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../server/src/ws/adapter/ws-redis.adapter.ts | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/apps/server/src/ws/adapter/ws-redis.adapter.ts b/apps/server/src/ws/adapter/ws-redis.adapter.ts index a221c84a..b72e35e2 100644 --- a/apps/server/src/ws/adapter/ws-redis.adapter.ts +++ b/apps/server/src/ws/adapter/ws-redis.adapter.ts @@ -11,6 +11,8 @@ import { export class WsRedisIoAdapter extends IoAdapter { private adapterConstructor: ReturnType; private redisConfig: RedisConfig; + private pubClient: Redis; + private subClient: Redis; async connectToRedis(): Promise { this.redisConfig = parseRedisUrl(process.env.REDIS_URL); @@ -26,6 +28,11 @@ export class WsRedisIoAdapter extends IoAdapter { pubClient.on('error', (err) => () => {}); subClient.on('error', (err) => () => {}); + // Hold references so the pub/sub connections can be torn down on shutdown + // (see dispose()); otherwise these ioredis sockets leak as active handles. + this.pubClient = pubClient; + this.subClient = subClient; + this.adapterConstructor = createAdapter(pubClient, subClient); } @@ -34,4 +41,26 @@ export class WsRedisIoAdapter extends IoAdapter { server.adapter(this.adapterConstructor); return server; } + + /** + * Called once by Nest's SocketModule during application shutdown, after every + * socket.io server has been closed. The @socket.io/redis-adapter never owns + * the lifecycle of the ioredis pub/sub clients it is handed, so we close them + * here to avoid leaking their TCP handles on shutdown (see issue #255). + * + * Uses disconnect(false) to mirror the sibling pub/sub pair in + * collaboration/extensions/redis-sync (redis-sync.extension.ts onDestroy): + * an immediate close with no graceful QUIT round-trip and no auto-reconnect, + * which is what we want for idle adapter clients during teardown. + */ + async dispose(): Promise { + await super.dispose(); + + // dispose() is invoked once per shutdown; null the refs so a second call + // (or any post-shutdown path) cannot act on already-closed clients. + this.pubClient?.disconnect(false); + this.subClient?.disconnect(false); + this.pubClient = undefined; + this.subClient = undefined; + } } -- 2.49.1 From 82b042209ef927a6b737a886b099ad5bb2c05ef1 Mon Sep 17 00:00:00 2001 From: claude code agent 227 Date: Mon, 29 Jun 2026 04:32:34 +0300 Subject: [PATCH 2/2] fix(ws): make redis adapter error handlers actually log (were noop) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The pub/sub error handlers were `(err) => () => {}` — a noop returning an inner arrow that never runs, so socket.io redis client errors were silently swallowed. Log them via Nest Logger. Adjacent pre-existing bug surfaced in review of #255. Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/server/src/ws/adapter/ws-redis.adapter.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/server/src/ws/adapter/ws-redis.adapter.ts b/apps/server/src/ws/adapter/ws-redis.adapter.ts index b72e35e2..4cc1c3e5 100644 --- a/apps/server/src/ws/adapter/ws-redis.adapter.ts +++ b/apps/server/src/ws/adapter/ws-redis.adapter.ts @@ -1,3 +1,4 @@ +import { Logger } from '@nestjs/common'; import { IoAdapter } from '@nestjs/platform-socket.io'; import { ServerOptions } from 'socket.io'; import { createAdapter } from '@socket.io/redis-adapter'; @@ -9,6 +10,7 @@ import { } from '../../common/helpers'; export class WsRedisIoAdapter extends IoAdapter { + private readonly logger = new Logger(WsRedisIoAdapter.name); private adapterConstructor: ReturnType; private redisConfig: RedisConfig; private pubClient: Redis; @@ -25,8 +27,8 @@ export class WsRedisIoAdapter extends IoAdapter { const pubClient = new Redis(process.env.REDIS_URL, options); const subClient = new Redis(process.env.REDIS_URL, options); - pubClient.on('error', (err) => () => {}); - subClient.on('error', (err) => () => {}); + pubClient.on('error', (err) => this.logger.error('socket.io redis pub client error', err)); + subClient.on('error', (err) => this.logger.error('socket.io redis sub client error', err)); // Hold references so the pub/sub connections can be torn down on shutdown // (see dispose()); otherwise these ioredis sockets leak as active handles. -- 2.49.1