Skip to content

Commit 3b7e4bf

Browse files
authored
fix: Updated undici instrumentation to fix crash with trying to calculate exclusive duration on a segment that no longer exists (#2884)
1 parent dd30ad7 commit 3b7e4bf

File tree

6 files changed

+102
-139
lines changed

6 files changed

+102
-139
lines changed

documentation/feature-flags.md

-6
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,6 @@ Any prerelease flags can be enabled or disabled in your agent config by adding a
2020
* Environment Variable: `NEW_RELIC_FEATURE_FLAG_REVERSE_NAMING_RULES`
2121
* Description: Naming rules are in forward order by default.
2222

23-
#### undici_async_tracking
24-
* Enabled by default: `true`
25-
* Configuration: `{ feature_flag: { undici_async_tracking: true|false }}`
26-
* Environment Variable: `NEW_RELIC_FEATURE_FLAG_UNDICI_ASYNC_TRACKING`
27-
* Description: If you have multiple undici requests being made in parallel, you may find some state issues if requests to an app are made with keep-alive. If so, *disabling* this flag will avoid these state issues, though at the cost of some broken segment nesting.
28-
2923
#### unresolved_promise_cleanup
3024
* Enabled by default: `true`
3125
* Configuration: `{ feature_flag: { unresolved_promise_cleanup: true|false }}`

lib/feature_flags.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ exports.prerelease = {
1313

1414
promise_segments: false,
1515
reverse_naming_rules: false,
16-
undici_async_tracking: true,
1716
unresolved_promise_cleanup: true,
1817
kafkajs_instrumentation: false
1918
}
@@ -36,6 +35,7 @@ exports.released = [
3635
'await_support',
3736
'certificate_bundle',
3837
'async_local_context',
38+
'undici_async_tracking',
3939
'undici_instrumentation',
4040
'aws_bedrock_instrumentation',
4141
'langchain_instrumentation'

lib/instrumentation/undici.js

+9-48
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@ const logger = require('../logger').child({ component: 'undici' })
1111
const NAMES = require('../metrics/names')
1212
const symbols = require('../symbols')
1313
// eslint-disable-next-line n/no-unsupported-features/node-builtins
14-
const { executionAsyncResource } = require('async_hooks')
15-
// eslint-disable-next-line n/no-unsupported-features/node-builtins
1614
const diagnosticsChannel = require('diagnostics_channel')
1715
const synthetics = require('../synthetics')
1816
const urltils = require('../util/urltils')
@@ -49,42 +47,6 @@ module.exports.unsubscribe = function unsubscribe() {
4947
})
5048
}
5149

52-
/**
53-
* Retrieves the current segment(parent in our context) and transaction from executionAsyncResource
54-
* or from context manager then adds to the executionAsyncResource for future
55-
* undici requests within same async context.
56-
*
57-
* It was found that when running concurrent undici requests
58-
* within a transaction that the parent segment would get out of sync
59-
* depending on the async context of the transaction. By using
60-
* `async_hooks.executionResource` it is more reliable.
61-
*
62-
* Note: However, if you have concurrent undici requests in a transaction
63-
* and the request to the transaction is using a keep alive there is a chance the
64-
* executionAsyncResource may be incorrect because of shared connections. To revert to a more
65-
* naive tracking of parent set `config.feature_flag.undici_async_tracking: false` and
66-
* it will just get the segment and transaction from the context manager.
67-
*
68-
* @param {Shim} shim instance of shim
69-
* @returns {TraceSegment} parent segment
70-
*/
71-
function getParentContext(shim) {
72-
const { config } = shim.agent
73-
if (config.feature_flag.undici_async_tracking) {
74-
const resource = executionAsyncResource()
75-
76-
if (!resource[symbols.parentSegment]) {
77-
const parent = shim.getSegment()
78-
resource[symbols.parentSegment] = parent
79-
const tx = shim.tracer.getTransaction()
80-
resource[symbols.transaction] = tx
81-
}
82-
return { segment: resource[symbols.parentSegment], transaction: resource[symbols.transaction] }
83-
}
84-
85-
return shim.tracer.getContext()
86-
}
87-
8850
/**
8951
* Injects relevant DT headers for the external request
9052
*
@@ -160,7 +122,7 @@ function createExternalSegment({ shim, request, segment }) {
160122
*/
161123
function requestCreateHook(shim, { request }) {
162124
const { config } = shim.agent
163-
const { transaction, segment } = getParentContext(shim)
125+
const { transaction, segment } = shim.tracer.getContext()
164126
request[symbols.parentSegment] = segment
165127
request[symbols.transaction] = transaction
166128
if (!(segment || transaction) || segment?.opaque) {
@@ -221,9 +183,8 @@ function requestHeadersHook(shim, { request, response }) {
221183
}
222184

223185
/**
224-
* Gets the active and parent from given ctx(request, client connector)
225-
* and ends active and restores parent to active. If an error exists
226-
* it will add the error to the transaction
186+
* Gets the active segment, parent segment and transaction from given ctx(request, client connector)
187+
* and ends segment and sets the previous parent segment as the active segment. If an error exists it will add the error to the transaction
227188
*
228189
* @param {Shim} shim instance of shim
229190
* @param {object} params object from undici hook
@@ -236,14 +197,14 @@ function endAndRestoreSegment(shim, { request, error }) {
236197
const tx = request[symbols.transaction]
237198
if (activeSegment) {
238199
activeSegment.end()
200+
}
239201

240-
if (error) {
241-
handleError(shim, tx, error)
242-
}
202+
if (error && tx) {
203+
handleError(shim, tx, error)
204+
}
243205

244-
if (parentSegment) {
245-
shim.setActiveSegment(parentSegment)
246-
}
206+
if (parentSegment) {
207+
shim.setActiveSegment(parentSegment)
247208
}
248209
}
249210

test/integration/instrumentation/fetch.test.js

+45-1
Original file line numberDiff line numberDiff line change
@@ -144,13 +144,57 @@ test('fetch', async function (t) {
144144
const [{ status }, { status: status2 }] = await Promise.all([req1, req2])
145145
assert.equal(status, 200)
146146
assert.equal(status2, 200)
147-
assertSegments(tx.trace, tx.trace.root, [`External/${HOST}/post`, `External/${HOST}/put`], {
147+
const postName = `External/${HOST}/post`
148+
const putName = `External/${HOST}/put`
149+
const postSegment = metrics.findSegment(tx.trace, tx.trace.root, postName)
150+
assert.equal(postSegment.parentId, tx.trace.root.id)
151+
const putSegment = metrics.findSegment(tx.trace, tx.trace.root, putName)
152+
// parent of put is the post segment because it is still the active one
153+
// not ideal, but our instrumentation does not play nice with diagnostic_channel
154+
// we're setting the active segment in the `undici:request:create` and restoring
155+
// the parent segment in the request end
156+
assert.equal(putSegment.parentId, postSegment.id)
157+
assertSegments(tx.trace, tx.trace.root, [postSegment, putSegment], {
148158
exact: false
149159
})
150160
tx.end()
151161
})
152162
})
153163

164+
await t.test('concurrent requests in diff transaction', async () => {
165+
const tx1 = helper.runInTransaction(agent, async (tx) => {
166+
const { status } = await fetch(`${REQUEST_URL}/post`, {
167+
method: 'POST',
168+
headers: {
169+
'Content-Type': 'application.json'
170+
},
171+
body: Buffer.from('{"key":"value"}')
172+
})
173+
assert.equal(status, 200)
174+
const postName = `External/${HOST}/post`
175+
const postSegment = metrics.findSegment(tx.trace, tx.trace.root, postName)
176+
assert.equal(postSegment.parentId, tx.trace.root.id)
177+
tx.end()
178+
})
179+
180+
const tx2 = helper.runInTransaction(agent, async(tx) => {
181+
const { status } = await fetch(`${REQUEST_URL}/put`, {
182+
method: 'PUT',
183+
headers: {
184+
'Content-Type': 'application.json'
185+
},
186+
body: Buffer.from('{"key":"value"}')
187+
})
188+
assert.equal(status, 200)
189+
const putName = `External/${HOST}/put`
190+
const putSegment = metrics.findSegment(tx.trace, tx.trace.root, putName)
191+
assert.equal(putSegment.parentId, tx.trace.root.id)
192+
tx.end()
193+
})
194+
195+
await Promise.all([tx1, tx2])
196+
})
197+
154198
await t.test('invalid host', async () => {
155199
await helper.runInTransaction(agent, async (tx) => {
156200
try {

test/unit/instrumentation/undici.test.js

-82
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,6 @@ test('undici instrumentation', async function (t) {
2727
const agent = helper.loadMockedAgent()
2828
agent.config.distributed_tracing.enabled = false
2929
agent.config.cross_application_tracer.enabled = false
30-
agent.config.feature_flag = {
31-
undici_async_tracking: true
32-
}
3330
const shim = new TransactionShim(agent, 'undici')
3431
const loggerMock = require('../mocks/logger')(sandbox)
3532
const undiciInstrumentation = proxyquire('../../../lib/instrumentation/undici', {
@@ -43,7 +40,6 @@ test('undici instrumentation', async function (t) {
4340
sandbox.resetHistory()
4441
agent.config.distributed_tracing.enabled = false
4542
agent.config.cross_application_tracer.enabled = false
46-
agent.config.feature_flag.undici_async_tracking = true
4743
helper.unloadAgent(agent)
4844
})
4945

@@ -130,84 +126,6 @@ test('undici instrumentation', async function (t) {
130126
}
131127
)
132128

133-
await t.test(
134-
'should get the parent segment executionAsyncResource when it already exists',
135-
function (t, end) {
136-
helper.runInTransaction(agent, function (tx) {
137-
const addHeader = sandbox.stub()
138-
const request = { origin: HOST, path: '/foo-2', addHeader }
139-
channels.create.publish({ request })
140-
const segment = tx.trace.add('another segment')
141-
segment.start()
142-
shim.setActiveSegment(segment)
143-
const request2 = { path: '/path', addHeader, origin: HOST }
144-
channels.create.publish({ request: request2 })
145-
assert.equal(
146-
request[symbols.parentSegment].id,
147-
request2[symbols.parentSegment].id,
148-
'parent segment should be same'
149-
)
150-
assert.equal(
151-
request[symbols.transaction].id,
152-
request2[symbols.transaction].id,
153-
'tx should be same'
154-
)
155-
tx.end()
156-
end()
157-
})
158-
}
159-
)
160-
161-
await t.test(
162-
'should get diff parent segment across diff async execution contexts',
163-
function (t, end) {
164-
helper.runInTransaction(agent, function (tx) {
165-
const request = { origin: HOST, path: '/request1', addHeader: sandbox.stub() }
166-
channels.create.publish({ request })
167-
Promise.resolve('test').then(() => {
168-
const segment = tx.trace.add('another segment')
169-
segment.start()
170-
shim.setActiveSegment(segment)
171-
const request2 = { path: '/request2', addHeader: sandbox.stub(), origin: HOST }
172-
channels.create.publish({ request: request2 })
173-
assert.notEqual(request[symbols.parentSegment], request2[symbols.parentSegment])
174-
assert.equal(request[symbols.transaction], request2[symbols.transaction])
175-
tx.end()
176-
end()
177-
})
178-
})
179-
}
180-
)
181-
182-
await t.test(
183-
'should get the parent segment shim when `undici_async_tracking` is false',
184-
function (t, end) {
185-
agent.config.feature_flag.undici_async_tracking = false
186-
helper.runInTransaction(agent, function (tx) {
187-
const addHeader = sandbox.stub()
188-
const request = { path: '/foo-2', addHeader, origin: HOST }
189-
channels.create.publish({ request })
190-
const segment = tx.trace.add('another segment')
191-
segment.start()
192-
shim.setActiveSegment(segment)
193-
const request2 = { path: '/path', addHeader, origin: HOST }
194-
channels.create.publish({ request: request2 })
195-
assert.notEqual(
196-
request[symbols.parentSegment].name,
197-
request2[symbols.parentSegment].name,
198-
'parent segment should not be same'
199-
)
200-
assert.equal(
201-
request[symbols.transaction].id,
202-
request2[symbols.transaction].id,
203-
'tx should be the same'
204-
)
205-
tx.end()
206-
end()
207-
})
208-
}
209-
)
210-
211129
await t.test(
212130
'should name segment with appropriate attrs based on request.path',
213131
function (t, end) {

test/versioned/undici/requests.test.js

+47-1
Original file line numberDiff line numberDiff line change
@@ -194,13 +194,59 @@ test('Undici request tests', async (t) => {
194194
const [{ statusCode }, { statusCode: statusCode2 }] = await Promise.all([req1, req2])
195195
assert.equal(statusCode, 200)
196196
assert.equal(statusCode2, 200)
197-
assertSegments(tx.trace, tx.trace.root, [`External/${HOST}/post`, `External/${HOST}/put`], {
197+
const postName = `External/${HOST}/post`
198+
const putName = `External/${HOST}/put`
199+
const postSegment = metrics.findSegment(tx.trace, tx.trace.root, postName)
200+
assert.equal(postSegment.parentId, tx.trace.root.id)
201+
const putSegment = metrics.findSegment(tx.trace, tx.trace.root, putName)
202+
// parent of put is the post segment because it is still the active one
203+
// not ideal, but our instrumentation does not play nice with diagnostic_channel
204+
// we're setting the active segment in the `undici:request:create` and restoring
205+
// the parent segment in the request end
206+
assert.equal(putSegment.parentId, postSegment.id)
207+
assertSegments(tx.trace, tx.trace.root, [postSegment, putSegment], {
198208
exact: false
199209
})
200210
tx.end()
201211
})
202212
})
203213

214+
await t.test('concurrent requests in diff transaction', async () => {
215+
const tx1 = helper.runInTransaction(agent, async (tx) => {
216+
const { statusCode } = await undici.request(REQUEST_URL, {
217+
path: '/post',
218+
method: 'POST',
219+
headers: {
220+
'Content-Type': 'application.json'
221+
},
222+
body: Buffer.from('{"key":"value"}')
223+
})
224+
assert.equal(statusCode, 200)
225+
const postName = `External/${HOST}/post`
226+
const postSegment = metrics.findSegment(tx.trace, tx.trace.root, postName)
227+
assert.equal(postSegment.parentId, tx.trace.root.id)
228+
tx.end()
229+
})
230+
231+
const tx2 = helper.runInTransaction(agent, async(tx) => {
232+
const { statusCode } = await undici.request(REQUEST_URL, {
233+
path: '/put',
234+
method: 'PUT',
235+
headers: {
236+
'Content-Type': 'application.json'
237+
},
238+
body: Buffer.from('{"key":"value"}')
239+
})
240+
assert.equal(statusCode, 200)
241+
const putName = `External/${HOST}/put`
242+
const putSegment = metrics.findSegment(tx.trace, tx.trace.root, putName)
243+
assert.equal(putSegment.parentId, tx.trace.root.id)
244+
tx.end()
245+
})
246+
247+
await Promise.all([tx1, tx2])
248+
})
249+
204250
await t.test('invalid host', async () => {
205251
await helper.runInTransaction(agent, async (tx) => {
206252
try {

0 commit comments

Comments
 (0)