Compare commits

..

2 Commits

Author SHA1 Message Date
claude code agent 227
82b042209e fix(ws): make redis adapter error handlers actually log (were noop)
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) <noreply@anthropic.com>
2026-06-29 04:32:34 +03:00
claude code agent 227
a0f4c86a74 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) <noreply@anthropic.com>
2026-06-29 04:28:56 +03:00

View File

@@ -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<typeof createAdapter>;
private redisConfig: RedisConfig;
private pubClient: Redis;
private subClient: Redis;
async connectToRedis(): Promise<void> {
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<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;
}
}