TimeoutStrategy leaks listeners
biggyspender opened this issue · comments
Repro in nodejs (with TS support via tsx):
import {
ExponentialBackoff,
TimeoutStrategy,
handleAll,
retry,
timeout,
wrap,
} from 'cockatiel';
const fail = (times: number) => {
let c = times;
return async () => {
if (c-- > 0) {
console.log('failing');
throw Error('fail');
}
console.log('success');
};
};
const timeoutPolicy = timeout(100, TimeoutStrategy.Cooperative);
const retryPolicy = retry(handleAll, {
backoff: new ExponentialBackoff({
initialDelay: 100,
exponent: 2,
maxDelay: 500,
}),
});
const policy = wrap(retryPolicy, timeoutPolicy);
const ac = new AbortController();
const func = fail(15);
policy.execute((ctx) => func(), ac.signal);
After about 11 retries, in the output, we see:
MaxListenersExceededWarning: Possible EventTarget memory leak detected. 11 abort listeners added to [AbortSignal]. Use events.setMaxListeners() to increase limit
A cursory read of the code suggests that listeners added via deriveAbortController
are not properly cleaned up in the case that the signal is not aborted (and there seems to be no mechanism by which these hanging listeners might be removed).
Here is a reproduction, running in StackBlitz (apparently only working in chromium-based browser for me 🤨 ).
I also note that toPromise
offers no mechanism have its listeners removed in the case that the signal doesn't abort. This isn't covered by the PR above, but would be caught by the following test:
const timeoutPolicy = timeout(10, TimeoutStrategy.Cooperative);
const ac = new AbortController();
let listenerCount = 0;
const sig = new Proxy(ac.signal, {
get: (signal, key, receiver) => {
const val = Reflect.get(signal, key, receiver);
if (key === 'addEventListener') {
return (...args: any[]) => {
listenerCount++;
return Reflect.apply(val, signal, args);
};
}
if (key === 'removeEventListener') {
return (...args: any[]) => {
listenerCount--;
return Reflect.apply(val, signal, args);
};
}
return val;
},
});
await timeoutPolicy.execute(() => Promise.resolve(), sig);
expect(listenerCount).to.eq(0);