Skip to content

Commit 9437d7e

Browse files
authored
fix: isolate binding EventEmitter (#1937)
Binding an EventEmitter a second time via another instances of ContextManager shouldn't have side effects to the first ContextManager. Use an unique symbol per ContextManager instance to isolate them.
1 parent 38d1ee2 commit 9437d7e

File tree

3 files changed

+126
-57
lines changed

3 files changed

+126
-57
lines changed

packages/opentelemetry-context-async-hooks/src/AbstractAsyncHooksContextManager.ts

Lines changed: 45 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,16 @@
1717
import { ContextManager, Context } from '@opentelemetry/context-base';
1818
import { EventEmitter } from 'events';
1919

20-
const kOtListeners = Symbol('OtListeners');
21-
2220
type Func<T> = (...args: unknown[]) => T;
2321

24-
type PatchedEventEmitter = {
25-
/**
26-
* Store a map for each event of all original listener and their "patched"
27-
* version so when the listener is removed by the user, we remove the
28-
* corresponding "patched" function.
29-
*/
30-
[kOtListeners]?: { [name: string]: WeakMap<Func<void>, Func<void>> };
31-
} & EventEmitter;
22+
/**
23+
* Store a map for each event of all original listeners and their "patched"
24+
* version. So when a listener is removed by the user, the corresponding
25+
* patched function will be also removed.
26+
*/
27+
interface PatchMap {
28+
[name: string]: WeakMap<Func<void>, Func<void>>;
29+
}
3230

3331
const ADD_LISTENER_METHODS = [
3432
'addListener' as const,
@@ -66,7 +64,7 @@ export abstract class AbstractAsyncHooksContextManager
6664

6765
private _bindFunction<T extends Function>(target: T, context: Context): T {
6866
const manager = this;
69-
const contextWrapper = function (this: {}, ...args: unknown[]) {
67+
const contextWrapper = function (this: never, ...args: unknown[]) {
7068
return manager.with(context, () => target.apply(this, args));
7169
};
7270
Object.defineProperty(contextWrapper, 'length', {
@@ -87,16 +85,16 @@ export abstract class AbstractAsyncHooksContextManager
8785
* By default, EventEmitter call their callback with their context, which we do
8886
* not want, instead we will bind a specific context to all callbacks that
8987
* go through it.
90-
* @param target EventEmitter a instance of EventEmitter to patch
88+
* @param ee EventEmitter an instance of EventEmitter to patch
9189
* @param context the context we want to bind
9290
*/
9391
private _bindEventEmitter<T extends EventEmitter>(
94-
target: T,
92+
ee: T,
9593
context: Context
9694
): T {
97-
const ee = (target as unknown) as PatchedEventEmitter;
98-
if (ee[kOtListeners] !== undefined) return target;
99-
ee[kOtListeners] = {};
95+
const map = this._getPatchMap(ee);
96+
if (map !== undefined) return ee;
97+
this._createPatchMap(ee);
10098

10199
// patch methods that add a listener to propagate context
102100
ADD_LISTENER_METHODS.forEach(methodName => {
@@ -117,7 +115,7 @@ export abstract class AbstractAsyncHooksContextManager
117115
ee.removeAllListeners
118116
);
119117
}
120-
return target;
118+
return ee;
121119
}
122120

123121
/**
@@ -126,9 +124,10 @@ export abstract class AbstractAsyncHooksContextManager
126124
* @param ee EventEmitter instance
127125
* @param original reference to the patched method
128126
*/
129-
private _patchRemoveListener(ee: PatchedEventEmitter, original: Function) {
130-
return function (this: {}, event: string, listener: Func<void>) {
131-
const events = ee[kOtListeners]?.[event];
127+
private _patchRemoveListener(ee: EventEmitter, original: Function) {
128+
const contextManager = this;
129+
return function (this: never, event: string, listener: Func<void>) {
130+
const events = contextManager._getPatchMap(ee)?.[event];
132131
if (events === undefined) {
133132
return original.call(this, event, listener);
134133
}
@@ -143,13 +142,12 @@ export abstract class AbstractAsyncHooksContextManager
143142
* @param ee EventEmitter instance
144143
* @param original reference to the patched method
145144
*/
146-
private _patchRemoveAllListeners(
147-
ee: PatchedEventEmitter,
148-
original: Function
149-
) {
150-
return function (this: {}, event: string) {
151-
if (ee[kOtListeners]?.[event] !== undefined) {
152-
delete ee[kOtListeners]![event];
145+
private _patchRemoveAllListeners(ee: EventEmitter, original: Function) {
146+
const contextManager = this;
147+
return function (this: never, event: string) {
148+
const map = contextManager._getPatchMap(ee);
149+
if (map?.[event] !== undefined) {
150+
delete map[event];
153151
}
154152
return original.call(this, event);
155153
};
@@ -163,22 +161,37 @@ export abstract class AbstractAsyncHooksContextManager
163161
* @param [context] context to propagate when calling listeners
164162
*/
165163
private _patchAddListener(
166-
ee: PatchedEventEmitter,
164+
ee: EventEmitter,
167165
original: Function,
168166
context: Context
169167
) {
170168
const contextManager = this;
171-
return function (this: {}, event: string, listener: Func<void>) {
172-
if (ee[kOtListeners] === undefined) ee[kOtListeners] = {};
173-
let listeners = ee[kOtListeners]![event];
169+
return function (this: never, event: string, listener: Func<void>) {
170+
let map = contextManager._getPatchMap(ee);
171+
if (map === undefined) {
172+
map = contextManager._createPatchMap(ee);
173+
}
174+
let listeners = map[event];
174175
if (listeners === undefined) {
175176
listeners = new WeakMap();
176-
ee[kOtListeners]![event] = listeners;
177+
map[event] = listeners;
177178
}
178179
const patchedListener = contextManager.bind(listener, context);
179180
// store a weak reference of the user listener to ours
180181
listeners.set(listener, patchedListener);
181182
return original.call(this, event, patchedListener);
182183
};
183184
}
185+
186+
private _createPatchMap(ee: EventEmitter): PatchMap {
187+
const map = {};
188+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
189+
(ee as any)[this._kOtListeners] = map;
190+
return map;
191+
}
192+
private _getPatchMap(ee: EventEmitter): PatchMap | undefined {
193+
return (ee as never)[this._kOtListeners];
194+
}
195+
196+
private readonly _kOtListeners = Symbol('OtListeners');
184197
}

packages/opentelemetry-context-async-hooks/src/AsyncLocalStorageContextManager.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export class AsyncLocalStorageContextManager extends AbstractAsyncHooksContextMa
3737
...args: A
3838
): ReturnType<F> {
3939
const cb = thisArg == null ? fn : fn.bind(thisArg);
40-
return this._asyncLocalStorage.run(context, cb as any, ...args);
40+
return this._asyncLocalStorage.run(context, cb as never, ...args);
4141
}
4242

4343
enable(): this {

packages/opentelemetry-context-async-hooks/test/AsyncHooksContextManager.test.ts

Lines changed: 80 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ for (const contextManagerClass of [
3131
| AsyncHooksContextManager
3232
| AsyncLocalStorageContextManager;
3333
const key1 = createContextKey('test key 1');
34+
let otherContextManager:
35+
| AsyncHooksContextManager
36+
| AsyncLocalStorageContextManager;
3437

3538
before(function () {
3639
if (
@@ -49,6 +52,7 @@ for (const contextManagerClass of [
4952

5053
afterEach(() => {
5154
contextManager.disable();
55+
otherContextManager?.disable();
5256
});
5357

5458
describe('.enable()', () => {
@@ -274,6 +278,22 @@ for (const contextManagerClass of [
274278
countDown();
275279
}, time2);
276280
});
281+
282+
it('should not influence other instances', () => {
283+
otherContextManager = new contextManagerClass();
284+
otherContextManager.enable();
285+
286+
const context = ROOT_CONTEXT.setValue(key1, 2);
287+
const otherContext = ROOT_CONTEXT.setValue(key1, 3);
288+
contextManager.with(context, () => {
289+
assert.strictEqual(contextManager.active(), context);
290+
assert.strictEqual(otherContextManager.active(), ROOT_CONTEXT);
291+
otherContextManager.with(otherContext, () => {
292+
assert.strictEqual(contextManager.active(), context);
293+
assert.strictEqual(otherContextManager.active(), otherContext);
294+
});
295+
});
296+
});
277297
});
278298

279299
describe('.bind(function)', () => {
@@ -335,6 +355,22 @@ for (const contextManagerClass of [
335355
}, context);
336356
fn();
337357
});
358+
359+
it('should not influence other instances', () => {
360+
otherContextManager = new contextManagerClass();
361+
otherContextManager.enable();
362+
363+
const context = ROOT_CONTEXT.setValue(key1, 2);
364+
const otherContext = ROOT_CONTEXT.setValue(key1, 3);
365+
const fn = otherContextManager.bind(
366+
contextManager.bind(() => {
367+
assert.strictEqual(contextManager.active(), context);
368+
assert.strictEqual(otherContextManager.active(), otherContext);
369+
}, context),
370+
otherContext
371+
);
372+
fn();
373+
});
338374
});
339375

340376
describe('.bind(event-emitter)', () => {
@@ -352,31 +388,31 @@ for (const contextManagerClass of [
352388
it('should return current context and removeListener (when enabled)', done => {
353389
const ee = new EventEmitter();
354390
const context = ROOT_CONTEXT.setValue(key1, 1);
355-
const patchedEe = contextManager.bind(ee, context);
391+
const patchedEE = contextManager.bind(ee, context);
356392
const handler = () => {
357393
assert.deepStrictEqual(contextManager.active(), context);
358-
patchedEe.removeListener('test', handler);
359-
assert.strictEqual(patchedEe.listeners('test').length, 0);
394+
patchedEE.removeListener('test', handler);
395+
assert.strictEqual(patchedEE.listeners('test').length, 0);
360396
return done();
361397
};
362-
patchedEe.on('test', handler);
363-
assert.strictEqual(patchedEe.listeners('test').length, 1);
364-
patchedEe.emit('test');
398+
patchedEE.on('test', handler);
399+
assert.strictEqual(patchedEE.listeners('test').length, 1);
400+
patchedEE.emit('test');
365401
});
366402

367403
it('should return current context and removeAllListener (when enabled)', done => {
368404
const ee = new EventEmitter();
369405
const context = ROOT_CONTEXT.setValue(key1, 1);
370-
const patchedEe = contextManager.bind(ee, context);
406+
const patchedEE = contextManager.bind(ee, context);
371407
const handler = () => {
372408
assert.deepStrictEqual(contextManager.active(), context);
373-
patchedEe.removeAllListeners('test');
374-
assert.strictEqual(patchedEe.listeners('test').length, 0);
409+
patchedEE.removeAllListeners('test');
410+
assert.strictEqual(patchedEE.listeners('test').length, 0);
375411
return done();
376412
};
377-
patchedEe.on('test', handler);
378-
assert.strictEqual(patchedEe.listeners('test').length, 1);
379-
patchedEe.emit('test');
413+
patchedEE.on('test', handler);
414+
assert.strictEqual(patchedEE.listeners('test').length, 1);
415+
patchedEE.emit('test');
380416
});
381417

382418
/**
@@ -387,34 +423,54 @@ for (const contextManagerClass of [
387423
contextManager.disable();
388424
const ee = new EventEmitter();
389425
const context = ROOT_CONTEXT.setValue(key1, 1);
390-
const patchedEe = contextManager.bind(ee, context);
426+
const patchedEE = contextManager.bind(ee, context);
391427
const handler = () => {
392428
assert.deepStrictEqual(contextManager.active(), context);
393-
patchedEe.removeListener('test', handler);
394-
assert.strictEqual(patchedEe.listeners('test').length, 0);
429+
patchedEE.removeListener('test', handler);
430+
assert.strictEqual(patchedEE.listeners('test').length, 0);
395431
return done();
396432
};
397-
patchedEe.on('test', handler);
398-
assert.strictEqual(patchedEe.listeners('test').length, 1);
399-
patchedEe.emit('test');
433+
patchedEE.on('test', handler);
434+
assert.strictEqual(patchedEE.listeners('test').length, 1);
435+
patchedEE.emit('test');
400436
});
401437

402438
it('should not return current context with async op', done => {
403439
const ee = new EventEmitter();
404440
const context = ROOT_CONTEXT.setValue(key1, 1);
405-
const patchedEe = contextManager.bind(ee, context);
441+
const patchedEE = contextManager.bind(ee, context);
406442
const handler = () => {
407443
assert.deepStrictEqual(contextManager.active(), context);
408444
setImmediate(() => {
409445
assert.deepStrictEqual(contextManager.active(), context);
410-
patchedEe.removeAllListeners('test');
411-
assert.strictEqual(patchedEe.listeners('test').length, 0);
446+
patchedEE.removeAllListeners('test');
447+
assert.strictEqual(patchedEE.listeners('test').length, 0);
412448
return done();
413449
});
414450
};
415-
patchedEe.on('test', handler);
416-
assert.strictEqual(patchedEe.listeners('test').length, 1);
417-
patchedEe.emit('test');
451+
patchedEE.on('test', handler);
452+
assert.strictEqual(patchedEE.listeners('test').length, 1);
453+
patchedEE.emit('test');
454+
});
455+
456+
it('should not influence other instances', () => {
457+
const ee = new EventEmitter();
458+
otherContextManager = new contextManagerClass();
459+
otherContextManager.enable();
460+
461+
const context = ROOT_CONTEXT.setValue(key1, 2);
462+
const otherContext = ROOT_CONTEXT.setValue(key1, 3);
463+
const patchedEE = otherContextManager.bind(
464+
contextManager.bind(ee, context),
465+
otherContext
466+
);
467+
const handler = () => {
468+
assert.strictEqual(contextManager.active(), context);
469+
assert.strictEqual(otherContextManager.active(), otherContext);
470+
};
471+
472+
patchedEE.on('test', handler);
473+
patchedEE.emit('test');
418474
});
419475
});
420476
});

0 commit comments

Comments
 (0)