-
Notifications
You must be signed in to change notification settings - Fork 227
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
Use async_hooks for Node.js 8.2.0 and above #77
Changes from 2 commits
5ec35f3
60e2bf7
63b4be0
74552f1
baff5e0
db9b120
6a384ff
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
'use strict' | ||
|
||
const asyncHooks = require('async_hooks') | ||
|
||
module.exports = function (ins) { | ||
const asyncHook = asyncHooks.createHook({init, destroy}) | ||
const transactions = new Map() | ||
|
||
Object.defineProperty(ins, 'currentTransaction', { | ||
get: function () { | ||
const asyncId = asyncHooks.executionAsyncId() | ||
return transactions.get(asyncId) | ||
}, | ||
set: function (trans) { | ||
const asyncId = asyncHooks.executionAsyncId() | ||
transactions.set(asyncId, trans) | ||
} | ||
}) | ||
|
||
asyncHook.enable() | ||
|
||
function init (asyncId, type, triggerAsyncId, resource) { | ||
// We don't care about the TIMERWRAP, as it will only init once for each | ||
// timer that shares the timeout value. Instead we rely on the Timeout | ||
// type, which will init for each scheduled timer. | ||
if (type === 'TIMERWRAP') return | ||
|
||
transactions.set(asyncId, ins.currentTransaction) | ||
} | ||
|
||
function destroy (asyncId) { | ||
if (!transactions.has(asyncId)) return // in case type === TIMERWRAP | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I imagine leaving this up to the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same as |
||
transactions.delete(asyncId) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
'use strict' | ||
|
||
var agent = require('../..').start({ | ||
appName: 'test', | ||
captureExceptions: false, | ||
ff_asyncHooks: true | ||
}) | ||
var ins = agent._instrumentation | ||
|
||
var test = require('tape') | ||
var semver = require('semver') | ||
|
||
test('setTimeout', function (t) { | ||
t.plan(2) | ||
twice(function () { | ||
var trans = agent.startTransaction() | ||
setTimeout(function () { | ||
t.equal(ins.currentTransaction && ins.currentTransaction.id, trans.id) | ||
}, 50) | ||
}) | ||
}) | ||
|
||
test('setInterval', function (t) { | ||
t.plan(2) | ||
twice(function () { | ||
var trans = agent.startTransaction() | ||
var timer = setInterval(function () { | ||
clearInterval(timer) | ||
t.equal(ins.currentTransaction && ins.currentTransaction.id, trans.id) | ||
}, 50) | ||
}) | ||
}) | ||
|
||
test('setImmediate', function (t) { | ||
t.plan(2) | ||
twice(function () { | ||
var trans = agent.startTransaction() | ||
setImmediate(function () { | ||
t.equal(ins.currentTransaction && ins.currentTransaction.id, trans.id) | ||
}) | ||
}) | ||
}) | ||
|
||
test('process.nextTick', function (t) { | ||
t.plan(2) | ||
twice(function () { | ||
var trans = agent.startTransaction() | ||
process.nextTick(function () { | ||
t.equal(ins.currentTransaction && ins.currentTransaction.id, trans.id) | ||
}) | ||
}) | ||
}) | ||
|
||
// We can't instrument ore-defined promises properly without async-hooks, so | ||
// lets not run these tests on versions of Node.js without async-hooks | ||
if (semver.gte(process.version, '8.1.0')) { | ||
test('pre-defined, pre-resolved shared promise', function (t) { | ||
t.plan(4) | ||
|
||
var p = Promise.resolve('success') | ||
|
||
twice(function () { | ||
var trans = agent.startTransaction() | ||
p.then(function (result) { | ||
t.equal(result, 'success') | ||
t.equal(ins.currentTransaction && ins.currentTransaction.id, trans.id) | ||
}) | ||
}) | ||
}) | ||
|
||
test('pre-defined, pre-resolved non-shared promise', function (t) { | ||
t.plan(4) | ||
|
||
twice(function () { | ||
var p = Promise.resolve('success') | ||
var trans = agent.startTransaction() | ||
p.then(function (result) { | ||
t.equal(result, 'success') | ||
t.equal(ins.currentTransaction && ins.currentTransaction.id, trans.id) | ||
}) | ||
}) | ||
}) | ||
|
||
test('pre-defined, post-resolved promise', function (t) { | ||
t.plan(4) | ||
twice(function () { | ||
var p = new Promise(function (resolve) { | ||
setTimeout(function () { | ||
resolve('success') | ||
}, 20) | ||
}) | ||
var trans = agent.startTransaction() | ||
p.then(function (result) { | ||
t.equal(result, 'success') | ||
t.equal(ins.currentTransaction && ins.currentTransaction.id, trans.id) | ||
}) | ||
}) | ||
}) | ||
} | ||
|
||
test('post-defined, post-resolved promise', function (t) { | ||
t.plan(4) | ||
twice(function () { | ||
var trans = agent.startTransaction() | ||
var p = new Promise(function (resolve) { | ||
setTimeout(function () { | ||
resolve('success') | ||
}, 20) | ||
}) | ||
p.then(function (result) { | ||
t.equal(result, 'success') | ||
t.equal(ins.currentTransaction && ins.currentTransaction.id, trans.id) | ||
}) | ||
}) | ||
}) | ||
|
||
function twice (fn) { | ||
setImmediate(fn) | ||
setImmediate(fn) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
'use strict' | ||
|
||
var agent = require('../..').start({ | ||
appName: 'test', | ||
captureExceptions: false, | ||
ff_asyncHooks: true | ||
}) | ||
|
||
var http = require('http') | ||
var send = require('send') | ||
var test = require('tape') | ||
|
||
// run it 5 times in case of false positives due to race conditions | ||
times(5, function (n, done) { | ||
test('https://github.com/elastic/apm-agent-nodejs/issues/75 ' + n, function (t) { | ||
resetAgent(function (endpoint, headers, data, cb) { | ||
t.equal(data.transactions.length, 2, 'should create transactions') | ||
data.transactions.forEach(function (trans) { | ||
t.equal(trans.traces.length, 1, 'transaction should have one trace') | ||
t.equal(trans.traces[0].name, trans.id, 'trace should belong to transaction') | ||
}) | ||
server.close() | ||
t.end() | ||
done() | ||
}) | ||
|
||
var server = http.createServer(function (req, res) { | ||
var trace = agent.buildTrace() | ||
trace.start(agent._instrumentation.currentTransaction.id) | ||
setTimeout(function () { | ||
trace.end() | ||
send(req, __filename).pipe(res) | ||
}, 50) | ||
}) | ||
|
||
var requestNo = 0 | ||
|
||
server.listen(function () { | ||
request() | ||
request() | ||
}) | ||
|
||
function request () { | ||
var port = server.address().port | ||
http.get('http://localhost:' + port, function (res) { | ||
res.on('end', function () { | ||
if (++requestNo === 2) { | ||
agent._instrumentation._queue._flush() | ||
} | ||
}) | ||
res.resume() | ||
}) | ||
} | ||
}) | ||
}) | ||
|
||
function times (max, fn) { | ||
var n = 0 | ||
run() | ||
function run () { | ||
if (++n > max) return | ||
fn(n, run) | ||
} | ||
} | ||
|
||
function resetAgent (cb) { | ||
agent._instrumentation.currentTransaction = null | ||
agent._instrumentation._queue._clear() | ||
agent._httpClient = { request: cb || function () {} } | ||
agent.captureError = function (err) { throw err } | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure if it's still the case, but previously
map.get(...)
could deopt if the item did not exist. It might be a good idea to put aif (!transactions.has(asyncId)) return
before the get.