fix(#255): disconnect socket.io redis-adapter pub/sub clients on shutdown #256

Open
agent_coder wants to merge 2 commits from fix/255-ws-redis-adapter-leak into develop

View File

@@ -1,3 +1,4 @@
import { Logger } from '@nestjs/common';
import { IoAdapter } from '@nestjs/platform-socket.io'; import { IoAdapter } from '@nestjs/platform-socket.io';
import { ServerOptions } from 'socket.io'; import { ServerOptions } from 'socket.io';
import { createAdapter } from '@socket.io/redis-adapter'; import { createAdapter } from '@socket.io/redis-adapter';
@@ -9,8 +10,11 @@ import {
} from '../../common/helpers'; } from '../../common/helpers';
export class WsRedisIoAdapter extends IoAdapter { export class WsRedisIoAdapter extends IoAdapter {
private readonly logger = new Logger(WsRedisIoAdapter.name);
private adapterConstructor: ReturnType<typeof createAdapter>; private adapterConstructor: ReturnType<typeof createAdapter>;
private redisConfig: RedisConfig; private redisConfig: RedisConfig;
private pubClient: Redis;
private subClient: Redis;
async connectToRedis(): Promise<void> { async connectToRedis(): Promise<void> {
this.redisConfig = parseRedisUrl(process.env.REDIS_URL); 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 pubClient = new Redis(process.env.REDIS_URL, options);
const subClient = new Redis(process.env.REDIS_URL, options); const subClient = new Redis(process.env.REDIS_URL, options);
pubClient.on('error', (err) => () => {}); pubClient.on('error', (err) => this.logger.error('socket.io redis pub client error', err));
subClient.on('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); this.adapterConstructor = createAdapter(pubClient, subClient);
} }
@@ -34,4 +43,26 @@ export class WsRedisIoAdapter extends IoAdapter {
server.adapter(this.adapterConstructor); server.adapter(this.adapterConstructor);
return server; 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<void> {
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;
}
} }