diff --git a/apps/server/src/ws/adapter/ws-redis.adapter.ts b/apps/server/src/ws/adapter/ws-redis.adapter.ts index a221c84a..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,8 +10,11 @@ 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; + private subClient: Redis; async connectToRedis(): Promise { this.redisConfig = parseRedisUrl(process.env.REDIS_URL); @@ -23,8 +27,13 @@ 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. + this.pubClient = pubClient; + this.subClient = subClient; this.adapterConstructor = createAdapter(pubClient, subClient); } @@ -34,4 +43,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; + } }