Skip to content

Commit

Permalink
add scope manager that uses the execution async resource for propagat…
Browse files Browse the repository at this point in the history
…ion (#1088)
  • Loading branch information
rochdev committed Oct 23, 2020
1 parent 5017d30 commit 77893d3
Show file tree
Hide file tree
Showing 8 changed files with 224 additions and 4 deletions.
1 change: 1 addition & 0 deletions benchmark/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@ const exec = cmd => execSync(cmd, { stdio: [0, 1, 2] })
exec('node benchmark/core')
exec('node benchmark/scope/async_hooks')
exec('node benchmark/scope/async_local_storage')
exec('node benchmark/scope/async_resource')
exec('node benchmark/platform/node')
exec('node benchmark/dd-trace')
92 changes: 92 additions & 0 deletions benchmark/scope/async_resource.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
'use strict'

const { AsyncResource } = require('async_hooks')

const platform = require('../../packages/dd-trace/src/platform')
const node = require('../../packages/dd-trace/src/platform/node')
const benchmark = require('../benchmark')

platform.use(node)

const suite = benchmark('scope (executionAsyncResource)')

const spanStub = require('../stubs/span')

const Scope = require('../../packages/dd-trace/src/scope/async_resource')

const scope = new Scope({
experimental: {}
})

function activateResource (name) {
return scope.activate(spanStub, () => {
return new AsyncResource(name, {
requireManualDestroy: true
})
})
}

suite
.add('Scope#activate', {
fn () {
const resource = activateResource('test')
resource.runInAsyncScope(() => {})
resource.emitDestroy()
}
})
.add('Scope#activate (nested)', {
fn () {
const outer = activateResource('outer')
let middle
let inner

outer.runInAsyncScope(() => {
middle = activateResource('middle')
})
outer.emitDestroy()

middle.runInAsyncScope(() => {
inner = activateResource('middle')
})
middle.emitDestroy()

inner.runInAsyncScope(() => {})
inner.emitDestroy()
}
})
.add('Scope#activate (async)', {
defer: true,
fn (deferred) {
scope.activate(spanStub, () => {
queueMicrotask(() => {
deferred.resolve()
})
})
}
})
.add('Scope#activate (promise)', {
defer: true,
fn (deferred) {
scope.activate(spanStub, () => {
Promise.resolve().then(() => {
deferred.resolve()
})
})
}
})
.add('Scope#activate (async/await)', {
defer: true,
async fn (deferred) {
await scope.activate(spanStub, () => {
return Promise.resolve()
})
deferred.resolve()
}
})
.add('Scope#active', {
fn () {
scope.active()
}
})

suite.run()
1 change: 1 addition & 0 deletions ext/scopes.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
declare const scopes: {
ASYNC_RESOURCE: 'async_resource',
ASYNC_LOCAL_STORAGE: 'async_local_storage',
ASYNC_HOOKS: 'async_hooks'
NOOP: 'noop'
Expand Down
1 change: 1 addition & 0 deletions ext/scopes.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict'

module.exports = {
ASYNC_RESOURCE: 'async_resource',
ASYNC_LOCAL_STORAGE: 'async_local_storage',
ASYNC_HOOKS: 'async_hooks',
NOOP: 'noop'
Expand Down
4 changes: 3 additions & 1 deletion packages/dd-trace/src/platform/node/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ const platform = {
off: emitter.removeListener.bind(emitter),
Loader,
getScope (scope) {
if (scope === scopes.ASYNC_LOCAL_STORAGE || (!scope && hasSupportedAsyncLocalStorage)) {
if (scope === scopes.ASYNC_RESOURCE) {
return require('../../scope/async_resource')
} else if (scope === scopes.ASYNC_LOCAL_STORAGE || (!scope && hasSupportedAsyncLocalStorage)) {
return require('../../scope/async_local_storage')
} else {
return require('../../scope/async_hooks')
Expand Down
72 changes: 72 additions & 0 deletions packages/dd-trace/src/scope/async_resource.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
'use strict'

const { createHook, executionAsyncResource } = require('async_hooks')
const Base = require('./base')

class Scope extends Base {
constructor (config) {
super()

this._ddResourceStore = Symbol('ddResourceStore')
this._config = config
this._stack = []
this._hook = createHook({
init: this._init.bind(this)
})

this.enable()
}

enable () {
this._enabled = true
this._hook.enable()
}

disable () {
this._enabled = false
this._stack = []
this._hook.disable()
}

_active () {
if (!this._enabled) return null

const resource = executionAsyncResource()

return resource[this._ddResourceStore] || null
}

_activate (span, callback) {
if (!this._enabled) return callback()

const resource = executionAsyncResource()

this._enter(span, resource)

try {
return callback()
} finally {
this._exit(resource)
}
}

_enter (span, resource) {
this._stack.push(resource[this._ddResourceStore])
resource[this._ddResourceStore] = span
}

_exit (resource) {
resource[this._ddResourceStore] = this._stack.pop()
}

_init (asyncId, type, triggerAsyncId, resource) {
const triggerResource = executionAsyncResource()
const span = triggerResource[this._ddResourceStore]

if (span) {
resource[this._ddResourceStore] = span
}
}
}

module.exports = Scope
47 changes: 47 additions & 0 deletions packages/dd-trace/test/scope/async_resource.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
'use strict'

const {
AsyncResource,
executionAsyncId
} = require('async_hooks')
const Scope = require('../../src/scope/async_resource')
const Span = require('opentracing').Span
const semver = require('semver')
const testScope = require('./test')

wrapIt('async_resource')

// https:// nodejs.org/api/async_hooks.html#async_hooks_async_hooks_executionasyncresource
if (semver.satisfies(process.version, '^12.17.0 || >=13.9.0')) {
describe('Scope (executionAsyncResource)', test)
} else {
describe.skip('Scope (executionAsyncResource)', test)
}

function test () {
let scope
let span

beforeEach(() => {
scope = new Scope()
span = new Span()
})

afterEach(() => {
scope.disable()
})

it('should not break propagation for nested resources', () => {
scope.activate(span, () => {
const asyncResource = new AsyncResource(
'TEST', { triggerAsyncId: executionAsyncId(), requireManualDestroy: false }
)

asyncResource.runInAsyncScope(() => {})

expect(scope.active()).to.equal(span)
})
})

testScope(() => scope)
}
10 changes: 7 additions & 3 deletions packages/dd-trace/test/setup/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const platform = require('../../src/platform')
const node = require('../../src/platform/node')
const AsyncHooksScope = require('../../src/scope/async_hooks')
const AsyncLocalStorageScope = require('../../src/scope/async_local_storage')
const AsyncResourceScope = require('../../src/scope/async_resource')
const agent = require('../plugins/agent')
const externals = require('../plugins/externals.json')

Expand All @@ -24,6 +25,7 @@ const asyncHooksScope = new AsyncHooksScope({
const asyncLocalStorageScope = defaultScope === 'async_hooks' ? null : new AsyncLocalStorageScope({
trackAsyncScope: true
})
const asyncResourceScope = defaultScope === 'async_hooks' ? null : new AsyncResourceScope()

chai.use(sinonChai)
chai.use(require('../asserts/profile'))
Expand All @@ -42,10 +44,12 @@ afterEach(() => {
})

function wrapIt (whichScope = defaultScope) {
const scope = {
const scopes = {
async_hooks: asyncHooksScope,
async_local_storage: asyncLocalStorageScope
}[whichScope]
async_local_storage: asyncLocalStorageScope,
async_resource: asyncResourceScope
}
const scope = scopes[whichScope] || scopes.async_hooks
const it = global.it
const only = global.it.only

Expand Down

0 comments on commit 77893d3

Please sign in to comment.