-
Notifications
You must be signed in to change notification settings - Fork 825
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(scope-manager): Add AsyncHooks implementations of ScopeManager
- Loading branch information
Showing
8 changed files
with
295 additions
and
3 deletions.
There are no files selected for viewing
File renamed without changes.
File renamed without changes.
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
158 changes: 158 additions & 0 deletions
158
packages/opentelemetry-scope-async-hooks/src/AsyncHooksScopeManager.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,158 @@ | ||
/** | ||
* Copyright 2019, OpenTelemetry Authors | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
import { ScopeManager } from '@opentelemetry/scope-base'; | ||
import * as asyncHooks from 'async_hooks'; | ||
import { EventEmitter } from 'events'; | ||
import * as shimmer from 'shimmer'; | ||
|
||
const EVENT_EMITTER_METHODS: Array<keyof EventEmitter> = [ | ||
'addListener', | ||
'on', | ||
'once', | ||
'prependListener', | ||
'prependOnceListener', | ||
]; | ||
|
||
export class AsyncHooksScopeManager implements ScopeManager { | ||
private asyncHook: asyncHooks.AsyncHook; | ||
private scopes: { [uid: number]: unknown } = {}; | ||
private current: unknown = null; | ||
|
||
constructor() { | ||
this.asyncHook = asyncHooks.createHook({ | ||
init: this.init.bind(this), | ||
before: this.before.bind(this), | ||
destroy: this.destroy.bind(this), | ||
}); | ||
} | ||
|
||
active(): unknown { | ||
return this.current; | ||
} | ||
|
||
with<T extends (...args: unknown[]) => ReturnType<T>>( | ||
scope: unknown, | ||
fn: T | ||
): ReturnType<T> { | ||
const oldCurrent = this.current; | ||
this.current = scope; | ||
const res = fn(); | ||
this.current = oldCurrent; | ||
return res; | ||
} | ||
|
||
bind<T>(target: T, scope?: unknown): T { | ||
// if no specific scope to propagate is give, we use the current one | ||
if (!scope) { | ||
scope = this.current; | ||
} | ||
if (target instanceof EventEmitter) { | ||
return this.bindEventEmitter(target, scope); | ||
} else if (typeof target === 'function') { | ||
return this.bindFunction(target, scope); | ||
} | ||
return target; | ||
} | ||
|
||
enable(): this { | ||
this.asyncHook.enable(); | ||
return this; | ||
} | ||
|
||
disable(): this { | ||
this.asyncHook.disable(); | ||
this.scopes = {}; | ||
this.current = null; | ||
return this; | ||
} | ||
|
||
private bindFunction<T extends Function>(target: T, scope?: unknown): T { | ||
const manager = this; | ||
const boundContext = scope; | ||
const contextWrapper = function(this: {}) { | ||
const oldContext = manager.current; | ||
manager.current = boundContext; | ||
const res = target.apply(this, arguments); | ||
manager.current = oldContext; | ||
return res; | ||
}; | ||
Object.defineProperty(contextWrapper, 'length', { | ||
enumerable: false, | ||
configurable: true, | ||
writable: false, | ||
value: target.length, | ||
}); | ||
/** | ||
* It isnt possible to tell Typescript that contextWrapper is the same as T | ||
* so we forced to cast as any here. | ||
*/ | ||
// tslint:disable-next-line:no-any | ||
return contextWrapper as any; | ||
} | ||
|
||
/** | ||
* By default, EventEmitter call their callback with their scope, which we do | ||
* not want, instead we will bind a specific scope to all callbacks that | ||
* go through it. | ||
* @param target EventEmitter a instance of EventEmitter to patch | ||
* @param scope the scope we want to bind | ||
*/ | ||
private bindEventEmitter<T extends EventEmitter>( | ||
target: T, | ||
scope?: unknown | ||
): T { | ||
const scopeManager = this; | ||
EVENT_EMITTER_METHODS.forEach(methodName => { | ||
if (target[methodName] === undefined) return; | ||
shimmer.wrap(target as EventEmitter, methodName, (original: Function) => { | ||
return function(this: {}, event: string, cb: Function) { | ||
return original.call(this, event, scopeManager.bind(cb, scope)); | ||
}; | ||
}); | ||
}); | ||
return target; | ||
} | ||
|
||
/** | ||
* Init hook will be called when userland create a async scope, setting the | ||
* scope as the current one if it exist. | ||
* @param uid id of the async scope | ||
*/ | ||
private init(uid: number, type: string) { | ||
this.scopes[uid] = this.current; | ||
} | ||
|
||
/** | ||
* Before hook will be called just before calling a callback on a ressource | ||
* so we restore the correct scope. | ||
* @param uid id of the async scope | ||
*/ | ||
private before(uid: number) { | ||
if (this.scopes[uid]) { | ||
this.current = this.scopes[uid]; | ||
} | ||
} | ||
|
||
/** | ||
* Destroy hook will be called when a given scope is no longer used so we can | ||
* remove its attached scope. | ||
* @param uid uid of the async scope | ||
*/ | ||
private destroy(uid: number) { | ||
delete this.scopes[uid]; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
127 changes: 127 additions & 0 deletions
127
packages/opentelemetry-scope-async-hooks/test/asynchooks/AsyncHooksScopeManager.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
/** | ||
* Copyright 2019, OpenTelemetry Authors | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
import * as assert from 'assert'; | ||
import { AsyncHooksScopeManager } from '../../src'; | ||
|
||
describe('AsyncHooksScopeManager', () => { | ||
let scopeManager: AsyncHooksScopeManager; | ||
|
||
describe('.enable()', () => { | ||
it('should work', () => { | ||
assert.doesNotThrow(() => { | ||
scopeManager = new AsyncHooksScopeManager(); | ||
assert(scopeManager.enable() === scopeManager, 'should return this'); | ||
}); | ||
}); | ||
}); | ||
|
||
describe('.disable()', () => { | ||
it('should work', () => { | ||
assert.doesNotThrow(() => { | ||
assert(scopeManager.disable() === scopeManager, 'should return this'); | ||
}); | ||
scopeManager.enable(); | ||
}); | ||
}); | ||
|
||
describe('.with()', () => { | ||
it('should run the callback (null as target)', done => { | ||
scopeManager.with(null, done); | ||
}); | ||
|
||
it('should run the callback (object as target)', done => { | ||
const test = { a: 1 }; | ||
scopeManager.with(test, () => { | ||
assert.strictEqual(scopeManager.active(), test, 'should have scope'); | ||
return done(); | ||
}); | ||
}); | ||
|
||
it('should run the callback (when disabled)', done => { | ||
scopeManager.disable(); | ||
scopeManager.with(null, () => { | ||
scopeManager.enable(); | ||
return done(); | ||
}); | ||
}); | ||
}); | ||
|
||
describe('.bind()', () => { | ||
it('should return the same target (when enabled)', () => { | ||
const test = { a: 1 }; | ||
assert.deepStrictEqual(scopeManager.bind(test), test); | ||
}); | ||
|
||
it('should return the same target (when disabled)', () => { | ||
scopeManager.disable(); | ||
const test = { a: 1 }; | ||
assert.deepStrictEqual(scopeManager.bind(test), test); | ||
scopeManager.enable(); | ||
}); | ||
|
||
it('should return current scope (when enabled)', done => { | ||
const scope = { a: 1 }; | ||
const fn = scopeManager.bind(() => { | ||
assert.strictEqual(scopeManager.active(), scope, 'should have scope'); | ||
return done(); | ||
}, scope); | ||
fn(); | ||
}); | ||
|
||
/** | ||
* Even if asynchooks is disabled, the scope propagation will | ||
* still works but it might be lost after any async op. | ||
*/ | ||
it('should return current scope (when disabled)', done => { | ||
scopeManager.disable(); | ||
const scope = { a: 1 }; | ||
const fn = scopeManager.bind(() => { | ||
assert.strictEqual(scopeManager.active(), scope, 'should have scope'); | ||
return done(); | ||
}, scope); | ||
fn(); | ||
}); | ||
|
||
it('should fail to return current scope (when disabled + async op)', done => { | ||
scopeManager.disable(); | ||
const scope = { a: 1 }; | ||
const fn = scopeManager.bind(() => { | ||
setTimeout(() => { | ||
assert.strictEqual( | ||
scopeManager.active(), | ||
null, | ||
'should have no scope' | ||
); | ||
return done(); | ||
}, 100); | ||
}, scope); | ||
fn(); | ||
}); | ||
|
||
it('should return current scope (when re-enabled + async op)', done => { | ||
scopeManager.enable(); | ||
const scope = { a: 1 }; | ||
const fn = scopeManager.bind(() => { | ||
setTimeout(() => { | ||
assert.strictEqual(scopeManager.active(), scope, 'should have scope'); | ||
return done(); | ||
}, 100); | ||
}, scope); | ||
fn(); | ||
}); | ||
}); | ||
}); |
File renamed without changes.