-
Notifications
You must be signed in to change notification settings - Fork 1.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
ioredis left open handles in Jest #1088
Comments
Hi, I am experiencing the same behaviour with ioredis and jest |
How about performing a redis.quit() or redis.disconnect() inside of your after or afterEach teardown methods? |
I'm facing the same issue and had to use jest's |
I think something like this in after should do the trick:
|
Having a similar issue where I am calling quit on all ioredis clients in my afterAll hook and it still is leaking events after the tests. |
I'm having a similar issue. I was able to work around it by continually checking the connection status. If it's not disconnected, just wait 200ms and check again, etc: redis.disconnect();
while (redis.status === "connected") {
await new Promise(r => setTimeout(r, 200));
} |
+1 with the issue here, had both |
I've tried both without any success too🤷♂️ |
I finally got it working, is anybody interested in a sample repo? |
I'd love to see a working solution. |
Let's compare. const teardown = async (redis: RedisClient) => {
await new Promise((resolve) => {
redis.quit();
redis.on('end', resolve);
});
};
afterAll(async () => {
await teardown(redis);
}); |
No need to compare @jaredjj3, you got it right :) afterAll(async () => {
await redis.quit()
}) I originally got the issue because I was wrapping it in a Nest.js service but I realized afterwards that I wasn't correctly handling lifecycle like : import IORedis, * as Redis from 'ioredis'
import { Injectable, OnApplicationBootstrap, OnApplicationShutdown } from '@nestjs/common'
@Injectable()
export class RedisService implements OnApplicationBootstrap, OnApplicationShutdown {
private redis: IORedis.Redis
// ...
async onApplicationBootstrap() {
redis = new Redis()
}
async onApplicationShutdown() {
await redis.quit()
}
} In the meantime I switched to Nest.js basic implementation which use redis under the hood and for which using promisify also made my life easier. |
@jsking216 I made a sample repo with tests for ioredis / redis / Nest.js 👌 |
Same with the new version 4.19 but with 4.17 all it's ok |
Supposed to be fixed in the latest release |
Found the same issue in Able to remove the error by using: afterAll(async () => {
await this.redis.quit();
}); |
for me, closing publisher helped (redis quit and disconnect was not enough)
|
@tot-ra how to get RedisPubSub ? |
Still having the same problem in our Test suites, any workable solution? |
@swkim0572 oops. sorry, it was imported from |
I am having the same problem and it has been giving me a headache, I don't understand why I still see " ioredis:redis write command[127.0.0.1:6379]: 0 -> brpoplpush([ 'bull:worker:wait', 'bull:worker:active', '5' ]) +5s" getting printed even when client.status is end |
I have the same issue. My implementation also involves Bull, and more specifically a blocking request like yours. It seems I can't async function redisClientDisconnect(client) {
if (client.status !== 'end') {
let _resolve, _reject;
return new Promise((resolve, reject) => {
_resolve = resolve;
_reject = reject;
client.once('end', _resolve);
pTimeout(
client.quit().catch(err => {
if (err.message !== 'Connection is closed.') {
throw err;
}
}),
500
).catch(() => {
client.once('error', _reject);
client.disconnect();
if (['connecting', 'reconnecting'].includes(client.status)) {
resolve();
}
});
}).finally(() => {
client.removeListener('end', _resolve);
client.removeListener('error', _reject);
});
}
} |
I dug into this a bit and setting DEBUG='redis:*' shows some interesting information. What I see is that the redis client transitions to end but prints a bit later than it is destroying the TCP connection:
So actually the redis instance has transitioned to state |
Looking around a bit, it seems the redis client called |
w.r.t. issues with bull keep in mind that it manages its own redis connections to some degree, so it's possible that even though you think you've closed the connection, there might be other redis client instances hanging around you hadn't closed. |
I think there might be a bug in here:
If the stream was closed already (e.g. using the |
Hmm no the bug was in my own code, arguably:
I was calling Sorry for the noise! |
This worked for me. bb |
hey, guys. I had the same issue. I was working around that. Not sure but what basically did was add the timeout. afterAll(async () => {
const ok = await redis.quit();
if (ok === 'OK') {
console.log('Redis connection closed');
}
}, 180000); // 3 minutes |
Looks like we already have solutions for the issue so closing. |
await new Promise<void>(resolve => {
redis.disconnect();
redis.on('end', resolve);
});
console.log(redis.status === 'end') This works |
Not sure if this will help some people, but something we found while working on this was that the act of loading up of the config/ Essentially, we had this cache config file setup like this: import { redisStore, RedisStore } from 'cache-manager-ioredis-yet';
import { registerAs } from '@nestjs/config';
export const CACHE_CONFIG = 'cache';
export interface CacheConfig {
store: RedisStore;
host: string;
port: number;
ttl: number;
}
export default registerAs(CACHE_CONFIG, async () => {
const host = // Host stuff
const port = // Port stuff
const ttl = // TTL stuff
return {
store: await redisStore({
host,
port,
ttl,
tls: // TLS stuff,
maxRetriesPerRequest: //Retries stuff,
retryStrategy(times) {
// retry strategy
},
}),
host,
port,
ttl,
} as CacheConfig;
}); Then we would load this config up into our application in a Common Module like this: import { ConfigModule, ConfigService } from '@nestjs/config';
import { Module } from '@nestjs/common';
import { CacheModule } from '@nestjs/cache-manager';
import cacheConfig, { CACHE_CONFIG, CacheConfig } from '@config/cache.config';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
load: [
cacheConfig,
// Other configs
],
}),
CacheModule.registerAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) =>
configService.get<CacheConfig>(CACHE_CONFIG),
inject: [ConfigService],
isGlobal: true,
}),
],
exports: [CacheModule, ConfigModule],
})
export class CommonModule {} What we ended up doing was utilizing Conditional Modules, and only loading up the Redis connection for when the system was not being tested. Here's what I mean: import { ConditionalModule, ConfigModule, ConfigService } from '@nestjs/config';
import { Module } from '@nestjs/common';
import { CacheModule } from '@nestjs/cache-manager';
import cacheConfig, { CACHE_CONFIG, CacheConfig } from '@config/cache.config';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
load: [
// Other configs - Note that the cache config isn't loaded here at all anymore
],
}),
// Note - Using the in-memory cache for tests
ConditionalModule.registerWhen(
CacheModule.register({ isGlobal: true }),
(env: NodeJS.ProcessEnv) => Boolean(env['TEST_ENVIRONMENT']),
),
ConditionalModule.registerWhen(
CacheModule.registerAsync({
// Note - Cache config is loaded up here but only for this particular feature
imports: [ConfigModule.forFeature(cacheConfig)],
inject: [ConfigService],
useFactory: async (configService: ConfigService) =>
configService.get<CacheConfig>(CACHE_CONFIG),
isGlobal: true,
}),
(env: NodeJS.ProcessEnv) => !Boolean(env['TEST_ENVIRONMENT']),
),
],
})
export class CommonModule {} |
Using close method /**
* @returns {Promise<[undefined, undefined, undefined]>}
*/
close() {
return Promise.all([
new Promise((resolve) => {
this.redis.removeAllListeners();
this.redis.once('end', resolve);
this.redis.quit();
this.redis.disconnect(false);
}),
new Promise((resolve) => {
this.subscriber.subRedis.removeAllListeners();
this.subscriber.subRedis.once('end', resolve);
this.subscriber.subRedis.quit();
this.subscriber.subRedis.disconnect(false);
}),
new Promise((resolve) => {
this.publisher.subRedis.removeAllListeners();
this.publisher.subRedis.once('end', resolve);
this.publisher.subRedis.quit();
this.publisher.subRedis.disconnect(false);
})
])
} even use both method quit and disconnect (or just quit or disconnect) that seem not close handles close method on jest afterAll(async () => {
return await instance.close()
}); error
|
Hello. I can't resolve the issue with my Jest tests because of ioredis. Recently I started recieving messages after Jest finished all tests:
After a few days of struggling I found the module called leaked-handles, installed it and started debugging. And it seems like both
redis.quit()
andredis.disconnect()
not working and connection remains opened after those calls. The error is:I couldn't find any mentioning of
ioredis
at Jest repo and viceversa. Is there anything that can be done to close connection properly?The text was updated successfully, but these errors were encountered: