Skip to content

Commit

Permalink
fix: more reliable span.sync determination, drop unused transaction.s…
Browse files Browse the repository at this point in the history
…ync (#2326)

Currently `span.sync` is tracked via:
- the "before" async hook setting it false on the "active" span, and
- Instrumentation#bindFunction's wrapper setting it false, on the
  fair assumption that all usages of bindFunction are for async
  callbacks.
The former has issues when there are multiple active spans within a
single async task -- as is the case with Elasticsearch instrumentation
(issue #1996) and the aws-sdk instrumentations (which have manual
workarounds).

This changes to set sync=false if the executionAsyncId() at end-time
is different than at start-time. This decouples from async context
management, so works for whatever value of the `asyncHooks`
config var.

This also drops the `transaction.sync` field which was never used.

Fixes: #1996
Fixes: #2292
  • Loading branch information
trentm authored Sep 20, 2021
1 parent 52d5ffc commit c9e4ca2
Show file tree
Hide file tree
Showing 15 changed files with 48 additions and 103 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ Notes:
[float]
===== Bug fixes
* Improve `span.sync` determination (fixes {issue}1996[#1996]) and stop
reporting `transaction.sync` which was never used ({issue}2292[#2292]).
A minor semantic change is that `span.sync` is not set to a final value
until `span.end()` is called. Before `span.end()` the value will always
by `true`.
* Guard against a negative value of `metricsInterval`, which can lead to
high CPU usage as metrics are collected as fast as possible. Also ensure
no metrics collection can happen if `metricsInterval="0s"` as intended.
Expand Down
8 changes: 0 additions & 8 deletions lib/instrumentation/async-hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,14 +92,6 @@ module.exports = function (ins) {
}

function before (asyncId) {
const span = activeSpans.get(asyncId)
if (span && !span.ended) {
span.sync = false
}
const transaction = span ? span.transaction : activeTransactions.get(asyncId)
if (transaction && !transaction.ended) {
transaction.sync = false
}
ins.bindingSpan = null
}

Expand Down
1 change: 0 additions & 1 deletion lib/instrumentation/generic-span.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ function GenericSpan (agent, ...args) {

this.timestamp = this._timer.start
this.ended = false
this.sync = true

this.outcome = constants.OUTCOME_UNKNOWN

Expand Down
8 changes: 1 addition & 7 deletions lib/instrumentation/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -353,11 +353,7 @@ var wrapped = Symbol('elastic-apm-wrapped-function')
// the callback function gets instrument on "the right" transaction and span.
//
// The instrumentation programmer is still responsible for starting a span,
// and ending a span. Additionally, this function will set a span's sync
// property to `false` -- it's up to the instrumentation programmer to ensure
// that the callback they're binding is really async. If bindFunction is
// passed a callback that the wrapped function executes synchronously, it will
// still mark the span's `async` property as `false`.
// and ending a span.
//
// @param {function} original
Instrumentation.prototype.bindFunction = function (original) {
Expand All @@ -379,8 +375,6 @@ Instrumentation.prototype.bindFunction = function (original) {
ins.currentTransaction = trans
ins.bindingSpan = null
ins.activeSpan = span
if (trans) trans.sync = false
if (span) span.sync = false
var result = original.apply(this, arguments)
ins.currentTransaction = prevTrans
return result
Expand Down
7 changes: 0 additions & 7 deletions lib/instrumentation/modules/aws-sdk/dynamodb.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,13 +100,6 @@ function instrumentationDynamoDb (orig, origArguments, request, AWS, agent, { ve
span._setOutcomeFromErrorCapture(constants.OUTCOME_FAILURE)
}

// Workaround a bug in the agent's handling of `span.sync`.
//
// The bug: Currently this span.sync is not set `false` because there is
// an HTTP span created (for this S3 request) in the same async op. That
// HTTP span becomes the "active span" for this async op, and *it* gets
// marked as sync=false in `before()` in async-hooks.js.
span.sync = false
span.end()
})

Expand Down
8 changes: 0 additions & 8 deletions lib/instrumentation/modules/aws-sdk/s3.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,14 +109,6 @@ function instrumentationS3 (orig, origArguments, request, AWS, agent, { version,
}
}

// Workaround a bug in the agent's handling of `span.sync`.
//
// The bug: Currently this span.sync is not set `false` because there is
// an HTTP span created (for this S3 request) in the same async op. That
// HTTP span becomes the "active span" for this async op, and *it* gets
// marked as sync=false in `before()` in async-hooks.js.
span.sync = false

span.end()
})
}
Expand Down
3 changes: 0 additions & 3 deletions lib/instrumentation/modules/aws-sdk/sns.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,6 @@ function instrumentationSns (orig, origArguments, request, AWS, agent, { version
span._setOutcomeFromErrorCapture(constants.OUTCOME_FAILURE)
}

// we'll need to manually mark this span as async. The actual async hop
// is captured by the agent's async hooks instrumentation
span.sync = false
span.end()
})

Expand Down
3 changes: 0 additions & 3 deletions lib/instrumentation/modules/aws-sdk/sqs.js
Original file line number Diff line number Diff line change
Expand Up @@ -166,9 +166,6 @@ function instrumentationSqs (orig, origArguments, request, AWS, agent, { version
span._setOutcomeFromErrorCapture(constants.OUTCOME_FAILURE)
}

// we'll need to manually mark this span as async. The actual async hop
// is captured by the agent's async hooks instrumentation
span.sync = false
span.end()

if (request.operation === 'receiveMessage' && response && response.data) {
Expand Down
6 changes: 6 additions & 0 deletions lib/instrumentation/span.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use strict'

const { executionAsyncId } = require('async_hooks')
var util = require('util')

var Value = require('async-value-promise')
Expand Down Expand Up @@ -34,6 +35,8 @@ function Span (transaction, name, ...args) {
this._message = null
this._stackObj = null
this._capturedStackTrace = null
this.sync = true
this._startXid = executionAsyncId()

this.transaction = transaction
this.name = name || 'unnamed'
Expand Down Expand Up @@ -71,6 +74,9 @@ Span.prototype.end = function (endTime) {
}

this._timer.end(endTime)
if (executionAsyncId() !== this._startXid) {
this.sync = false
}

this._setOutcomeFromSpanEnd()

Expand Down
1 change: 0 additions & 1 deletion lib/instrumentation/transaction.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,6 @@ Transaction.prototype.toJSON = function () {
result: String(this.result),
sampled: this.sampled,
context: undefined,
sync: this.sync,
span_count: {
started: this._builtSpans
},
Expand Down
37 changes: 0 additions & 37 deletions test/instrumentation/async-hooks.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,43 +121,6 @@ test('post-defined, post-resolved promise', function (t) {
})
})

test('sync/async tracking: span ended in same function is sync', function (t) {
var trans = agent.startTransaction()
t.strictEqual(trans.sync, true)

var span1 = agent.startSpan()
t.strictEqual(span1.sync, true)

// This span will be *ended* synchronously. It should stay `span.sync=true`.
var span2 = agent.startSpan()
t.strictEqual(span2.sync, true, 'span2.sync=true immediately after creation')
span2.end()
t.strictEqual(span2.sync, true, 'span2.sync=true immediately after end')

setImmediate(() => {
t.strictEqual(trans.sync, false)
t.strictEqual(span2.sync, true,
'span2.sync=true later after having ended sync')
t.end()
})
})

test('sync/async tracking: span ended across async boundary is not sync', function (t) {
var trans = agent.startTransaction()
t.strictEqual(trans.sync, true)

var span1 = agent.startSpan()
t.strictEqual(span1.sync, true)

setImmediate(() => {
span1.end()
t.strictEqual(trans.sync, false)
t.strictEqual(span1.sync, false,
'span1.sync=true after having ended sync')
t.end()
})
})

function twice (fn) {
setImmediate(fn)
setImmediate(fn)
Expand Down
2 changes: 2 additions & 0 deletions test/instrumentation/modules/@elastic/elasticsearch.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -586,12 +586,14 @@ function checkDataAndEnd (t, method, path, dbStatement) {
t.strictEqual(esSpan.type, 'db')
t.strictEqual(esSpan.subtype, 'elasticsearch')
t.strictEqual(esSpan.action, 'request')
t.strictEqual(esSpan.sync, false, 'span.sync=false')

const httpSpan = findObjInArray(data.spans, 'subtype', 'http')
t.ok(httpSpan, 'have an http span')
t.strictEqual(httpSpan.type, 'external')
t.strictEqual(httpSpan.subtype, 'http')
t.strictEqual(httpSpan.action, method)
t.strictEqual(httpSpan.sync, false, 'span.sync=false')

t.equal(httpSpan.name, method + ' ' + host, 'http span should have expected name')
t.equal(esSpan.name, 'Elasticsearch: ' + method + ' ' + path, 'elasticsearch span should have expected name')
Expand Down
1 change: 1 addition & 0 deletions test/instrumentation/modules/aws-sdk/sns.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ tape.test('AWS SNS: End to End Test', function (test) {
t.equals(span.type, 'messaging', 'span type correctly set')
t.equals(span.subtype, 'sns', 'span subtype set correctly')
t.equals(span.action, 'publish', 'span action set correctly')
t.equals(span.sync, false, 'span.sync is false')
t.equals(span.context.message.queue.name, 'topic-name')
t.equals(span.context.destination.service.resource, 'sns/topic-name')
t.equals(span.context.destination.service.type, 'messaging')
Expand Down
22 changes: 17 additions & 5 deletions test/instrumentation/span.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -161,12 +161,24 @@ test('#addLabels', function (t) {
t.end()
})

test('sync/async tracking', function (t) {
var trans = new Transaction(agent)
var span = new Span(trans)
t.strictEqual(span.sync, true)
test('span.sync', function (t) {
var trans = agent.startTransaction()

var span1 = agent.startSpan('span1')
t.strictEqual(span1.sync, true)

// This span will be *ended* synchronously. It should stay `span.sync=true`.
var span2 = agent.startSpan('span2')
t.strictEqual(span2.sync, true, 'span2.sync=true immediately after creation')
span2.end()
t.strictEqual(span2.sync, true, 'span2.sync=true immediately after end')

setImmediate(() => {
t.strictEqual(span.sync, false)
span1.end()
t.strictEqual(span1.sync, false, 'span1.sync=false immediately after end')
trans.end()
t.strictEqual(span2.sync, true,
'span2.sync=true later after having ended sync')
t.end()
})
})
Expand Down
Loading

0 comments on commit c9e4ca2

Please sign in to comment.