fix(server): close leaked redis sockets so e2e jest exits (#252)
The full-AppModule e2e (apps/server/test/app.e2e-spec.ts) passed but jest never exited, burning CI to its timeout. Diagnosis (process._getActiveHandles after app.close()) showed exactly two ioredis sockets to :6379 still open after shutdown; everything else (BullMQ queues/workers, @nestjs/schedule intervals, nestjs-ioredis, nestjs-kysely pg pool, @nestjs/cache-manager Keyv store, hocuspocus pub/sub) already closes on app.close(). The two leaks were owned-but-never-closed clients: 1. ThrottleModule passed a pre-built `new Redis(...)` instance to ThrottlerStorageRedisService. With an instance, the lib sets disconnectRequired=false, so its onModuleDestroy never disconnects. Pass ioredis options instead so the service owns + disconnects the client. 2. CollaborationGateway created a source `new RedisClient(...)` that RedisSyncExtension only duplicates into pub/sub; the extension's onDestroy disconnects those duplicates but not the source. Keep a reference and disconnect it after the hocuspocus onDestroy hook in destroy(). Both are real lifecycle fixes (production shutdown is now clean too), so no --forceExit is needed. Verified against real Postgres+Redis: - test:e2e (no forceExit, --runInBand) exits 0 in ~18s (was: hung forever) - --detectOpenHandles exits 0 with no open-handle report - active handles after app.close(): none CI timeout-minutes safety nets left untouched. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -10,7 +10,6 @@ import {
|
||||
PAGE_TEMPLATE_THROTTLER,
|
||||
PUBLIC_SHARE_AI_THROTTLER,
|
||||
} from './throttler-names';
|
||||
import Redis from 'ioredis';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -32,16 +31,18 @@ import Redis from 'ioredis';
|
||||
{ name: PUBLIC_SHARE_AI_THROTTLER, ttl: 60_000, limit: 5 },
|
||||
],
|
||||
errorMessage: 'Too many requests',
|
||||
storage: new ThrottlerStorageRedisService(
|
||||
new Redis({
|
||||
host: redisConfig.host,
|
||||
port: redisConfig.port,
|
||||
password: redisConfig.password,
|
||||
db: redisConfig.db,
|
||||
family: redisConfig.family,
|
||||
keyPrefix: 'throttle:',
|
||||
}),
|
||||
),
|
||||
// Pass ioredis options (not a pre-built Redis instance) so
|
||||
// ThrottlerStorageRedisService owns the connection and disconnects it
|
||||
// in its onModuleDestroy. Passing an instance leaves disconnectRequired
|
||||
// false, so the socket would leak on shutdown (e2e jest never exits).
|
||||
storage: new ThrottlerStorageRedisService({
|
||||
host: redisConfig.host,
|
||||
port: redisConfig.port,
|
||||
password: redisConfig.password,
|
||||
db: redisConfig.db,
|
||||
family: redisConfig.family,
|
||||
keyPrefix: 'throttle:',
|
||||
}),
|
||||
};
|
||||
},
|
||||
inject: [EnvironmentService],
|
||||
|
||||
Reference in New Issue
Block a user