diff --git a/apps/server/src/collaboration/collaboration.gateway.ts b/apps/server/src/collaboration/collaboration.gateway.ts index b46c13c8..b11f14b1 100644 --- a/apps/server/src/collaboration/collaboration.gateway.ts +++ b/apps/server/src/collaboration/collaboration.gateway.ts @@ -33,6 +33,11 @@ export class CollaborationGateway { // @ts-ignore private readonly redisSync: RedisSyncExtension | null = null; + // Source ioredis client that RedisSyncExtension duplicates into its pub/sub + // pair. The extension's onDestroy only disconnects those duplicates, so we + // keep a reference here and disconnect the source ourselves on shutdown + // (otherwise the socket leaks and jest never exits in e2e). + private redisClient: RedisClient | null = null; private readonly withRedis: boolean; constructor( @@ -57,16 +62,17 @@ export class CollaborationGateway { }); if (this.withRedis) { + this.redisClient = new RedisClient({ + host: this.redisConfig.host, + port: this.redisConfig.port, + password: this.redisConfig.password, + db: this.redisConfig.db, + family: this.redisConfig.family, + retryStrategy: createRetryStrategy(), + }); // @ts-ignore this.redisSync = new RedisSyncExtension({ - redis: new RedisClient({ - host: this.redisConfig.host, - port: this.redisConfig.port, - password: this.redisConfig.password, - db: this.redisConfig.db, - family: this.redisConfig.family, - retryStrategy: createRetryStrategy(), - }), + redis: this.redisClient, serverId: `collab-${os?.hostname()}-${nanoid(10)}`, prefix: 'collab', pack, @@ -184,5 +190,10 @@ export class CollaborationGateway { }); await this.hocuspocus.hooks('onDestroy', { instance: this.hocuspocus }); + + // RedisSyncExtension.onDestroy (run via the hook above) disconnects only the + // duplicated pub/sub clients; the source client created here is ours to close. + this.redisClient?.disconnect(); + this.redisClient = null; } } diff --git a/apps/server/src/integrations/throttle/throttle.module.ts b/apps/server/src/integrations/throttle/throttle.module.ts index 1cb0c41a..db197015 100644 --- a/apps/server/src/integrations/throttle/throttle.module.ts +++ b/apps/server/src/integrations/throttle/throttle.module.ts @@ -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],