Skip to content
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

events.on AsyncIterator API does not remove abort listener from signal in closeHandler #51010

Closed
nbbeeken opened this issue Dec 2, 2023 · 0 comments · Fixed by #51091
Closed

Comments

@nbbeeken
Copy link
Contributor

nbbeeken commented Dec 2, 2023

Version

v20.10.0

Platform

Microsoft Windows NT 10.0.19045.0 x64

Subsystem

events

What steps will reproduce the bug?

Hello!
The AbortSignal provided to the events.on API adds an abort listener and does not remove it after the iterator is complete. This causes a warning about a leak if on() is called enough times with the same signal.

Unfortunately, removeEventListener() cannot succeed without a reference to the original function.

Bear with the code sample size, tracking the calls to add and remove listeners required a bit of preamble:

import assert from 'node:assert/strict';
import { EventEmitter, on } from 'node:events';

class SpiedAbort {
  removeListenerCalls = 0;
  addListenerCalls = 0;

  constructor() {
    this.controller = new AbortController();
    const { signal } = this.controller;

    const originalAddEventListener = signal.addEventListener.bind(signal);
    signal.addEventListener = (...args) => {
      this.addListenerCalls += 1;
      return originalAddEventListener(...args);
    };

    const originalRemoveEventListener = signal.removeEventListener.bind(signal);
    signal.removeEventListener = (...args) => {
      this.removeListenerCalls += 1;
      return originalRemoveEventListener(...args);
    };
  }
}

const spiedAbort = new SpiedAbort();
const ee = new EventEmitter();
const iter = on(ee, 'myEvent', { signal: spiedAbort.controller.signal });

iter.return(); // Iterator cleaned up

assert.ok(
  spiedAbort.addListenerCalls >= 1,
  'expected at least one abort listener'
);
assert.equal(
  spiedAbort.removeListenerCalls,
  spiedAbort.addListenerCalls,
  'expected abort listener to be removed as many times as it was added'
);

How often does it reproduce? Is there a required condition?

Every time on() is invoked, a new listener is added, and when the iterator completes it will not be removed.

What is the expected behavior? Why is that the expected behavior?

I would expect the abort listener to be removed when the iterator is complete, regardless of success or failure.

What do you see instead?

When using the same signal across on calls:

import { EventEmitter, on } from 'node:events';
const ee = new EventEmitter();
const controller = new AbortController();
const { signal } = controller;

setInterval(() => ee.emit('myEvent', 1), 1);

for (let i = 0; i < 11; i++) {
  for await (const [data] of on(ee, 'myEvent', { signal })) break;
}

The MaxListenersExceededWarning is printed:

(node:27304) MaxListenersExceededWarning: Possible EventTarget memory leak detected. 11 abort listeners added to [AbortSignal]. Use events.setMaxListeners() to increase limit
    at [kNewListener] (node:internal/event_target:560:17)
    at [kNewListener] (node:internal/abort_controller:241:24)
    at EventTarget.addEventListener (node:internal/event_target:673:23)
    at eventTargetAgnosticAddListener (node:events:1030:13)
    at on (node:events:1159:5)
    at file:///leak.mjs:11:30

Additional information

The referenced closeHandler() can be found here:

node/lib/events.js

Lines 1203 to 1204 in 60ffa9f

function closeHandler() {
removeAll();

nbbeeken added a commit to nbbeeken/node that referenced this issue Dec 2, 2023
The async iterator returned by the events.on API
now cleans up the abort listener when the iterator ends
regardless of success or failure.

Fixes: nodejs#51010
nbbeeken added a commit to nbbeeken/node that referenced this issue Dec 20, 2023
Given an AbortSignal when passed into the events.on AsyncIterator API
the attached abort listener should always be removed when the iterator
completes.

The abortHandler function is declared within the scope of
the events.on function so cannot be removed by the caller which can lead
to a memory leak. Adding the abort listener using the addEventListener
helper returned by the listenersController helper adds the abort listener
to the array of listeners that will be cleaned up by removeAll in the
closeHandler for the AsyncIterator.

Fixes: nodejs#51010
nbbeeken added a commit to nbbeeken/node that referenced this issue Jan 10, 2024
Given an AbortSignal when passed into the events.on AsyncIterator API
the attached abort listener should always be removed when the iterator
completes.

The abortHandler function is declared within the scope of
the events.on function so cannot be removed by the caller which can lead
to a memory leak. Adding the abort listener using the addEventListener
helper returned by the listenersController helper adds the abort listener
to the array of listeners that will be cleaned up by removeAll in the
closeHandler for the AsyncIterator.

Fixes: nodejs#51010
nbbeeken added a commit to nbbeeken/node that referenced this issue Jan 27, 2024
Given an AbortSignal when passed into the events.on AsyncIterator API
the attached abort listener should always be removed when the iterator
completes.

The abortHandler function is declared within the scope of
the events.on function so cannot be removed by the caller which can lead
to a memory leak. Adding the abort listener using the addEventListener
helper returned by the listenersController helper adds the abort listener
to the array of listeners that will be cleaned up by removeAll in the
closeHandler for the AsyncIterator.

Fixes: nodejs#51010
nbbeeken added a commit to nbbeeken/node that referenced this issue Mar 1, 2024
Given an AbortSignal when passed into the events.on AsyncIterator API
the attached abort listener should always be removed when the iterator
completes.

The abortHandler function is declared within the scope of
the events.on function so cannot be removed by the caller which can lead
to a memory leak. Adding the abort listener using the addEventListener
helper returned by the listenersController helper adds the abort listener
to the array of listeners that will be cleaned up by removeAll in the
closeHandler for the AsyncIterator.

Fixes: nodejs#51010
nbbeeken added a commit to nbbeeken/node that referenced this issue Mar 13, 2024
Given an AbortSignal when passed into the events.on AsyncIterator API
the attached abort listener should always be removed when the iterator
completes.

The abortHandler function is declared within the scope of
the events.on function so cannot be removed by the caller which can lead
to a memory leak. Adding the abort listener using the addEventListener
helper returned by the listenersController helper adds the abort listener
to the array of listeners that will be cleaned up by removeAll in the
closeHandler for the AsyncIterator.

Fixes: nodejs#51010
nbbeeken added a commit to nbbeeken/node that referenced this issue Mar 14, 2024
the `abortHandler` function is declared within the scope of
the `events.on` function so cannot be removed by the caller
which can lead to a memory leak
adding the abort listener using the `addAbortListener` helper
returns a disposable that can be used to clean up the listener
when the iterator is exited

Fixes: nodejs#51010
nodejs-github-bot pushed a commit that referenced this issue Mar 15, 2024
the `abortHandler` function is declared within the scope of
the `events.on` function so cannot be removed by the caller
which can lead to a memory leak
adding the abort listener using the `addAbortListener` helper
returns a disposable that can be used to clean up the listener
when the iterator is exited

Fixes: #51010
PR-URL: #51091
Reviewed-By: Chemi Atlow <chemi@atlow.co.il>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
rdw-msft pushed a commit to rdw-msft/node that referenced this issue Mar 26, 2024
the `abortHandler` function is declared within the scope of
the `events.on` function so cannot be removed by the caller
which can lead to a memory leak
adding the abort listener using the `addAbortListener` helper
returns a disposable that can be used to clean up the listener
when the iterator is exited

Fixes: nodejs#51010
PR-URL: nodejs#51091
Reviewed-By: Chemi Atlow <chemi@atlow.co.il>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
marco-ippolito pushed a commit that referenced this issue May 2, 2024
the `abortHandler` function is declared within the scope of
the `events.on` function so cannot be removed by the caller
which can lead to a memory leak
adding the abort listener using the `addAbortListener` helper
returns a disposable that can be used to clean up the listener
when the iterator is exited

Fixes: #51010
PR-URL: #51091
Reviewed-By: Chemi Atlow <chemi@atlow.co.il>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
marco-ippolito pushed a commit that referenced this issue May 3, 2024
the `abortHandler` function is declared within the scope of
the `events.on` function so cannot be removed by the caller
which can lead to a memory leak
adding the abort listener using the `addAbortListener` helper
returns a disposable that can be used to clean up the listener
when the iterator is exited

Fixes: #51010
PR-URL: #51091
Reviewed-By: Chemi Atlow <chemi@atlow.co.il>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
jcbhmr pushed a commit to jcbhmr/node that referenced this issue May 15, 2024
the `abortHandler` function is declared within the scope of
the `events.on` function so cannot be removed by the caller
which can lead to a memory leak
adding the abort listener using the `addAbortListener` helper
returns a disposable that can be used to clean up the listener
when the iterator is exited

Fixes: nodejs#51010
PR-URL: nodejs#51091
Reviewed-By: Chemi Atlow <chemi@atlow.co.il>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

1 participant