Skip to content

Commit 0b7e083

Browse files
vmarchaudmayurkale22
authored andcommitted
feat(scope-manager): implements AsyncHooks Scope Manager (open-telemetry#103)
* feat(scope-manager): Add AsyncHooks implementations of ScopeManager * fix(ah-scope): remove shimmer, handle methods that remove listeners * test(ah-scope): add tests for binding scope on event emitter * feat(ah-scope): handle bind with promise + tests * chore(ah-scope): update automatic tracer dependency to new name * chore(ah-scope): remove support for event emitter and promises for now * chore(ah-scope): address pr comments
1 parent af8a7e3 commit 0b7e083

File tree

9 files changed

+256
-4
lines changed

9 files changed

+256
-4
lines changed

packages/opentelemetry-node-tracer/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
},
5151
"dependencies": {
5252
"@opentelemetry/core": "^0.0.1",
53-
"@opentelemetry/context-async-hooks": "^0.0.1",
53+
"@opentelemetry/scope-async-hooks": "^0.0.1",
5454
"@opentelemetry/scope-base": "^0.0.1",
5555
"@opentelemetry/types": "^0.0.1"
5656
}

packages/opentelemetry-context-async-hooks/package.json packages/opentelemetry-scope-async-hooks/package.json

+7-3
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
{
2-
"name": "@opentelemetry/context-async-hooks",
2+
"name": "@opentelemetry/scope-async-hooks",
33
"version": "0.0.1",
4-
"description": "OpenTelemetry AsyncHooks-based Context Manager",
4+
"description": "OpenTelemetry AsyncHooks-based Scope Manager",
55
"main": "build/src/index.js",
66
"types": "build/src/index.d.ts",
77
"repository": "open-telemetry/opentelemetry-js",
88
"scripts": {
99
"test": "c8 ts-mocha -p tsconfig.json test/**/*.ts",
1010
"tdd": "yarn test -- --watch-extensions ts --watch",
11+
"codecov": "c8 report --reporter=json && codecov -f coverage/*.json -p ../../",
1112
"clean": "rimraf build/*",
1213
"check": "gts check",
1314
"compile": "tsc -p .",
@@ -40,6 +41,7 @@
4041
"devDependencies": {
4142
"@types/mocha": "^5.2.5",
4243
"@types/node": "^12.0.10",
44+
"@types/shimmer": "^1.0.1",
4345
"c8": "^5.0.1",
4446
"codecov": "^3.1.0",
4547
"gts": "^1.0.0",
@@ -48,5 +50,7 @@
4850
"ts-node": "^8.0.0",
4951
"typescript": "^3.4.5"
5052
},
51-
"dependencies": {}
53+
"dependencies": {
54+
"@opentelemetry/scope-base": "^0.0.1"
55+
}
5256
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/**
2+
* Copyright 2019, OpenTelemetry Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { ScopeManager } from '@opentelemetry/scope-base';
18+
import * as asyncHooks from 'async_hooks';
19+
20+
export class AsyncHooksScopeManager implements ScopeManager {
21+
private _asyncHook: asyncHooks.AsyncHook;
22+
private _scopes: { [uid: number]: unknown } = Object.create(null);
23+
24+
constructor() {
25+
this._asyncHook = asyncHooks.createHook({
26+
init: this._init.bind(this),
27+
destroy: this._destroy.bind(this),
28+
promiseResolve: this._destroy.bind(this),
29+
});
30+
}
31+
32+
active(): unknown {
33+
return this._scopes[asyncHooks.executionAsyncId()] || null;
34+
}
35+
36+
with<T extends (...args: unknown[]) => ReturnType<T>>(
37+
scope: unknown,
38+
fn: T
39+
): ReturnType<T> {
40+
const uid = asyncHooks.executionAsyncId();
41+
const oldScope = this._scopes[uid];
42+
this._scopes[uid] = scope;
43+
try {
44+
return fn();
45+
} catch (err) {
46+
throw err;
47+
} finally {
48+
if (oldScope === undefined) {
49+
this._destroy(uid);
50+
} else {
51+
this._scopes[uid] = oldScope;
52+
}
53+
}
54+
}
55+
56+
bind<T>(target: T, scope?: unknown): T {
57+
// if no specific scope to propagate is given, we use the current one
58+
if (scope === undefined) {
59+
scope = this.active();
60+
}
61+
if (typeof target === 'function') {
62+
return this._bindFunction(target, scope);
63+
}
64+
return target;
65+
}
66+
67+
enable(): this {
68+
this._asyncHook.enable();
69+
return this;
70+
}
71+
72+
disable(): this {
73+
this._asyncHook.disable();
74+
this._scopes = {};
75+
return this;
76+
}
77+
78+
private _bindFunction<T extends Function>(target: T, scope?: unknown): T {
79+
const manager = this;
80+
const contextWrapper = function(this: {}) {
81+
return manager.with(scope, () => target.apply(this, arguments));
82+
};
83+
Object.defineProperty(contextWrapper, 'length', {
84+
enumerable: false,
85+
configurable: true,
86+
writable: false,
87+
value: target.length,
88+
});
89+
/**
90+
* It isnt possible to tell Typescript that contextWrapper is the same as T
91+
* so we forced to cast as any here.
92+
*/
93+
// tslint:disable-next-line:no-any
94+
return contextWrapper as any;
95+
}
96+
97+
/**
98+
* Init hook will be called when userland create a async scope, setting the
99+
* scope as the current one if it exist.
100+
* @param uid id of the async scope
101+
*/
102+
private _init(uid: number) {
103+
this._scopes[uid] = this._scopes[asyncHooks.executionAsyncId()];
104+
}
105+
106+
/**
107+
* Destroy hook will be called when a given scope is no longer used so we can
108+
* remove its attached scope.
109+
* @param uid uid of the async scope
110+
*/
111+
private _destroy(uid: number) {
112+
delete this._scopes[uid];
113+
}
114+
}

packages/opentelemetry-context-async-hooks/src/index.ts packages/opentelemetry-scope-async-hooks/src/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,5 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16+
17+
export * from './AsyncHooksScopeManager';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
/**
2+
* Copyright 2019, OpenTelemetry Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import * as assert from 'assert';
18+
import { AsyncHooksScopeManager } from '../../src';
19+
20+
describe('AsyncHooksScopeManager', () => {
21+
let scopeManager: AsyncHooksScopeManager;
22+
23+
afterEach(() => {
24+
scopeManager.disable();
25+
scopeManager.enable();
26+
});
27+
28+
describe('.enable()', () => {
29+
it('should work', () => {
30+
assert.doesNotThrow(() => {
31+
scopeManager = new AsyncHooksScopeManager();
32+
assert(scopeManager.enable() === scopeManager, 'should return this');
33+
});
34+
});
35+
});
36+
37+
describe('.disable()', () => {
38+
it('should work', () => {
39+
assert.doesNotThrow(() => {
40+
assert(scopeManager.disable() === scopeManager, 'should return this');
41+
});
42+
scopeManager.enable();
43+
});
44+
});
45+
46+
describe('.with()', () => {
47+
it('should run the callback (null as target)', done => {
48+
scopeManager.with(null, done);
49+
});
50+
51+
it('should run the callback (object as target)', done => {
52+
const test = { a: 1 };
53+
scopeManager.with(test, () => {
54+
assert.strictEqual(scopeManager.active(), test, 'should have scope');
55+
return done();
56+
});
57+
});
58+
59+
it('should run the callback (when disabled)', done => {
60+
scopeManager.disable();
61+
scopeManager.with(null, () => {
62+
scopeManager.enable();
63+
return done();
64+
});
65+
});
66+
});
67+
68+
describe('.bind(function)', () => {
69+
it('should return the same target (when enabled)', () => {
70+
const test = { a: 1 };
71+
assert.deepStrictEqual(scopeManager.bind(test), test);
72+
});
73+
74+
it('should return the same target (when disabled)', () => {
75+
scopeManager.disable();
76+
const test = { a: 1 };
77+
assert.deepStrictEqual(scopeManager.bind(test), test);
78+
scopeManager.enable();
79+
});
80+
81+
it('should return current scope (when enabled)', done => {
82+
const scope = { a: 1 };
83+
const fn = scopeManager.bind(() => {
84+
assert.strictEqual(scopeManager.active(), scope, 'should have scope');
85+
return done();
86+
}, scope);
87+
fn();
88+
});
89+
90+
/**
91+
* Even if asynchooks is disabled, the scope propagation will
92+
* still works but it might be lost after any async op.
93+
*/
94+
it('should return current scope (when disabled)', done => {
95+
scopeManager.disable();
96+
const scope = { a: 1 };
97+
const fn = scopeManager.bind(() => {
98+
assert.strictEqual(scopeManager.active(), scope, 'should have scope');
99+
return done();
100+
}, scope);
101+
fn();
102+
});
103+
104+
it('should fail to return current scope (when disabled + async op)', done => {
105+
scopeManager.disable();
106+
const scope = { a: 1 };
107+
const fn = scopeManager.bind(() => {
108+
setTimeout(() => {
109+
assert.strictEqual(
110+
scopeManager.active(),
111+
null,
112+
'should have no scope'
113+
);
114+
return done();
115+
}, 100);
116+
}, scope);
117+
fn();
118+
});
119+
120+
it('should return current scope (when re-enabled + async op)', done => {
121+
scopeManager.enable();
122+
const scope = { a: 1 };
123+
const fn = scopeManager.bind(() => {
124+
setTimeout(() => {
125+
assert.strictEqual(scopeManager.active(), scope, 'should have scope');
126+
return done();
127+
}, 100);
128+
}, scope);
129+
fn();
130+
});
131+
});
132+
});

0 commit comments

Comments
 (0)