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] 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; + } }