-
Notifications
You must be signed in to change notification settings - Fork 853
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
AbstractAsyncHooksContextManager breaks .removeListener for handlers which was added with .once #2971
Comments
I have been looking into this and I think this might be an issue within the Minimal repro: const { EventEmitter } = require("events");
const assert = require("assert")
// global map because who cares for the example
const map = {};
function wrapFunction(target) {
return function (...args) {
target.apply(this, args);
};
}
function patchAddListener(original) {
return function (event, listener) {
let listeners = map[event];
if (listeners === undefined) {
listeners = {}
map[event] = listeners;
}
const patchedListener = wrapFunction(listener);
listeners[listener] = patchedListener;
return original.call(this, event, patchedListener);
}
}
function patchOff(original) {
return function (event, listener) {
const listeners = map[event];
if (listeners === undefined) {
return original.call(this, event, listener);
}
const patchedListener = listeners[listener];
return original.call(this, event, patchedListener || listener);
}
}
ee = new EventEmitter();
// Comment this line to fix the assertions
ee.on = patchAddListener(ee.on);
ee.once = patchAddListener(ee.once);
ee.removeListener = patchOff(ee.removeListener);
const handler = () => { console.log('handler') };
ee.once('test', handler);
assert.strictEqual(ee.listeners('test').length, 1);
ee.removeListener('test', handler);
assert.strictEqual(ee.listeners('test').length, 0); The only way around this I can think of is for us to completely reimplement |
/cc @Flarna since you're probably our best expert on node internals |
I think I may have discovered the issue.
Proof of the theory: const { EventEmitter } = require("events");
const assert = require("assert")
const patchedFnSymbol = Symbol('OpenTelemetry patched function');
function wrapFunction(target) {
if (typeof target !== 'function') {
return target;
}
if (target[patchedFnSymbol]) {
// this line is never called because when `on` is called, it is called with the `onceWrapper` not with our wrapper
console.log('this is already wrapped')
return target[patchedFnSymbol];
}
console.log('wrapping', target)
const patchedFn = function patchedFunction(...args) {
console.log('this function is wrapped');
target.apply(this, args);
};
target[patchedFnSymbol] = patchedFn;
return patchedFn
}
function getWrapper(target) {
if (typeof target !== 'function' || typeof target[patchedFnSymbol] !== 'function') {
return target;
}
return getWrapper(target[patchedFnSymbol]);
}
function patchAddListener(ee, name) {
const original = ee[name]
const wrapper = function (event, listener) {
if (listener && typeof listener == 'object') {
if (typeof listener.listener == 'function') {
listener = listener.listener;
}
}
const patchedListener = wrapFunction(listener);
console.log(`calling original ${name} with`, patchedListener);
return original.call(this, event, patchedListener);
}
ee[name] = wrapper;
}
function patchRemoveListener(original) {
return function (event, listener) {
const wrapper = getWrapper(listener);
console.log('calling original removeListener with', wrapper);
return original.call(this, event, wrapper);
}
}
ee = new EventEmitter();
// Comment this line to fix the assertions
patchAddListener(ee, 'on');
patchAddListener(ee, 'once');
ee.removeListener = patchRemoveListener(ee.removeListener);
const handler = () => { console.log('handler') };
ee.once('test', handler);
assert.strictEqual(ee.listeners('test').length, 1);
ee.removeListener('test', handler);
assert.strictEqual(ee.listeners('test').length, 0); Console output:
|
in const { EventEmitter } = require("events");
const assert = require("assert")
const patchedFnSymbol = Symbol('OpenTelemetry patched function');
function wrapFunction(target) {
if (typeof target !== 'function') {
return target;
}
// IMPORTANT LINE HERE
if (target.name === 'bound onceWrapper') {
return target;
}
if (target[patchedFnSymbol]) {
// this line is never called because when `on` is called, it is called with the `onceWrapper` not with our wrapper
console.log('this is already wrapped')
return target[patchedFnSymbol];
}
console.log('wrapping', target)
const patchedFn = function patchedFunction(...args) {
console.log('this function is wrapped');
target.apply(this, args);
};
target[patchedFnSymbol] = patchedFn;
return patchedFn
}
function getWrapper(target) {
if (typeof target !== 'function' || typeof target[patchedFnSymbol] !== 'function') {
return target;
}
return getWrapper(target[patchedFnSymbol]);
}
function patchAddListener(ee, name) {
const original = ee[name]
const wrapper = function (event, listener) {
if (listener && typeof listener == 'object') {
if (typeof listener.listener == 'function') {
listener = listener.listener;
}
}
const patchedListener = wrapFunction(listener);
console.log(`calling original ${name} with`, patchedListener);
return original.call(this, event, patchedListener);
}
ee[name] = wrapper;
}
function patchRemoveListener(original) {
return function (event, listener) {
const wrapper = getWrapper(listener);
console.log('calling original removeListener with', wrapper);
return original.call(this, event, wrapper);
}
}
ee = new EventEmitter();
// Comment this line to fix the assertions
patchAddListener(ee, 'on');
patchAddListener(ee, 'once');
ee.removeListener = patchRemoveListener(ee.removeListener);
const handler = () => { console.log('handler') };
ee.once('test', handler);
assert.strictEqual(ee.listeners('test').length, 1);
ee.removeListener('test', handler);
assert.strictEqual(ee.listeners('test').length, 0); console output:
|
If anybody knows a better way to check if the function is a bound onceWrapper please tell me |
Here is the original implementation for function onceWrapper() {
if (!this.fired) {
this.target.removeListener(this.type, this.wrapFn);
this.fired = true;
if (arguments.length === 0)
return this.listener.call(this.target);
return this.listener.apply(this.target, arguments);
}
}
function _onceWrap(target, type, listener) {
const state = { fired: false, wrapFn: undefined, target, type, listener };
const wrapped = onceWrapper.bind(state);
wrapped.listener = listener;
state.wrapFn = wrapped;
return wrapped;
}
/**
* Adds a one-time `listener` function to the event emitter.
* @param {string | symbol} type
* @param {Function} listener
* @returns {EventEmitter}
*/
EventEmitter.prototype.once = function once(type, listener) {
checkListener(listener);
this.on(type, _onceWrap(this, type, listener));
return this;
}; |
Sorry for the late answer, was on vacation. I think special handling of The really hard task is implement it in a way that it still works (and doesn't break others) if there are other context managers (e.g. APM tools,...) used at the same time which also patch Note that besides |
What version of OpenTelemetry are you using?
latest
@opentelemetry/context-async-hooks 1.2.0
What version of Node are you using?
v16.14.0
Please provide the code you used to setup the OpenTelemetry SDK
Code uses
req.once("abort", handler)
and after successful processing it callsreq.removeListener("abort", handler)
. Code works without OTEL. With OTEL removeListener is not removing handler and application crashes with unhandled error.What did you do?
I have created unit tests which reproduces the problem: #2970
Once bug is fixed, it should pass the test.
Code
What did you expect to see?
Test should pass. Handler should be removed if we call once, and then removeListener.
What did you see instead?
Handler is not removed from listeners.
Additional context
#2970
The text was updated successfully, but these errors were encountered: