Skip to content
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

fix: span compression bug where a buffered span would not be sent when an incompressible sibling ended #3076

Merged
merged 4 commits into from
Dec 20, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"license-header/header": [ "error", "./dev-utils/license-header.js" ]
},
"ignorePatterns": [
"/*.example.js", // a pattern for uncommited local dev files to avoid linting
"/.nyc_output",
"/build",
"node_modules",
Expand Down
19 changes: 19 additions & 0 deletions CHANGELOG.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,25 @@ Notes:
=== Node.js Agent version 3.x


==== Unreleased

[float]
===== Breaking changes

[float]
===== Features

[float]
===== Bug fixes

* Fix a bug in span compression with sending spans that were buffered for
possible compression. Before this fix a compressible span could be sent
*twice* or not sent at all. ({pull}3076[#3076])

[float]
===== Chores


[[release-notes-3.41.0]]
==== 3.41.0 2022/12/12

Expand Down
18 changes: 10 additions & 8 deletions lib/instrumentation/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -414,24 +414,26 @@ Instrumentation.prototype.addEndedSpan = function (span) {
// if I have ended and I have something buffered, send that buffered thing
if (span.getBufferedSpan()) {
this._encodeAndSendSpan(span.getBufferedSpan())
span.setBufferedSpan(null)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

REVIEW NOTE: This clearing avoids the bug where we would encode and send a span twice -- the first time here, the second time in the next if-block.

}

if (!span.isCompressionEligible() || span.getParentSpan().ended) {
const buffered = span.getBufferedSpan()
const parentSpan = span.getParentSpan()
if ((parentSpan && parentSpan.ended) || !span.isCompressionEligible()) {
const buffered = parentSpan && parentSpan.getBufferedSpan()
if (buffered) {
this._encodeAndSendSpan(buffered)
span.setBufferedSpan(null)
parentSpan.setBufferedSpan(null)
Comment on lines +422 to +425
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

REVIEW NOTE: This corrects the logic to make the spec pseudo code: this block is meant to be able looking at the possibly buffered span on the parent. This fixes the case where a buffered span would not get sent if it was followed by an incompressible sibling span.

}
this._encodeAndSendSpan(span)
} else if (!span.getParentSpan().getBufferedSpan()) {
} else if (!parentSpan.getBufferedSpan()) {
// span is compressible and there's nothing buffered
// add to buffer, move on
span.getParentSpan().setBufferedSpan(span)
} else if (!span.getParentSpan().getBufferedSpan().tryToCompress(span)) {
parentSpan.setBufferedSpan(span)
} else if (!parentSpan.getBufferedSpan().tryToCompress(span)) {
// we could not compress span so SEND bufferend span
// and buffer the span we could not compress
this._encodeAndSendSpan(span.getParentSpan().getBufferedSpan())
span.getParentSpan().setBufferedSpan(span)
this._encodeAndSendSpan(parentSpan.getBufferedSpan())
parentSpan.setBufferedSpan(span)
}
}
}
Expand Down
51 changes: 51 additions & 0 deletions test/instrumentation/span-compression.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,57 @@ tape.test('integration/end-to-end span compression tests', function (suite) {
}, 300)
})

// The scenario is as follows:
// trans t0
// span s1: _bufferdSpan=s2
// span s2: ended, compressible -> buffered on s1
// span s3: incompressible, _bufferdSpan=s4
// span s4: ended, compressible -> buffered on s3
//
// What happens when s3 ends? We expect:
// - s4 is encoded and sent
// - s2 is encoded and sent
// - s3 is encoded and sent
//
// This tests the following from https://github.com/elastic/apm/blob/main/specs/agents/handling-huge-traces/tracing-spans-compress.md#span-buffering
// > A buffered span gets reported when
// > 1. its parent ends
// > 2. a non-compressible sibling ends
suite.test('ensure a span buffered on parent is sent when incompressible span ends', function (t) {
resetAgent(function (data) {
// We expect sent spans for: s4, s2, and s3; in that order.
t.equals(data.length, 3, '3 events sent')
t.equals(data.spans.length, 3, '3 spans sent')
t.equals(data.spans.map(s => s.name).join(','), 's4,s2,s3',
'sent spans are s4,s2,s3; in that order')
t.end()
})

const t0 = agent.startTransaction('t0')
setImmediate(() => {
const s1 = agent.startSpan('s1', 'manual')

setImmediate(() => {
const s2 = agent.startSpan('s2', 'db', 'mysql', { exitSpan: true })
t.equal(s2.getParentSpan().name, 's1', 's2 is a child of s1')
s2.end()
t.equal(s1.getBufferedSpan().name, 's2', 's2 is buffered on s1')

const s3 = t0.startSpan('s3', 'manual') // incompressible (because exitSpan=false)
t.equal(s3.getParentSpan().name, 's1', 's3 is a child of s1')

setImmediate(() => {
const s4 = agent.startSpan('s4', 'db', 'mysql', { exitSpan: true })
t.equal(s4.getParentSpan().name, 's3', 's4 is a child of s3')
s4.end()
t.equal(s3.getBufferedSpan().name, 's4', 's4 is buffered on s3')

s3.end()
})
})
})
})

suite.end()
})

Expand Down