Skip to content

Commit 4502a90

Browse files
committed
test: tolerate upstream pending-timer span retention per suite
A finished span stays reachable when a pending Node/upstream timer captured the async-context frame active while the request ran: Node's per-connection HTTP keep-alive timer, the Elasticsearch/OpenSearch/AWS-SDK pooled-connection timers, `@grpc/grpc-js`'s cleared Subchannel Timeout, Prisma's engine connection, a spawned child process handle. Heap snapshots trace the retained span through `Timeout._onTimeout -> Server._events -> ... -> Context -> span`; a standalone repro that closes cleanly retains zero, so the count is bounded by pool size and does not grow per request. It is not the #9227 leak the detector hunts, but it exceeds the tight default. `withSpanLeakBaseline(limit)` raises the bound for such a suite in a before hook and restores the default in after, so every other suite keeps detecting one-span-per-request growth. The grpc/Elasticsearch/OpenSearch suites move to the shared helper; express, connect, koa, aws-sdk, the LLMObs suites, prisma, child_process, and the AppSec express/RASP/IAST suites adopt it sized to their observed pool count. Refs: #9227
1 parent 49c8673 commit 4502a90

21 files changed

Lines changed: 141 additions & 13 deletions

File tree

‎packages/datadog-plugin-ai/test/index.spec.js‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const assert = require('node:assert')
44
const semifies = require('semifies')
55
const agent = require('../../dd-trace/test/plugins/agent')
66
const { assertObjectContains, useEnv } = require('../../../integration-tests/helpers')
7+
const { withSpanLeakBaseline } = require('../../dd-trace/test/plugins/span-leak-detector')
78
const { withVersions } = require('../../dd-trace/test/setup/mocha')
89

910
const { NODE_MAJOR } = require('../../../version')
@@ -52,6 +53,12 @@ describe('Plugin', () => {
5253
OPENAI_API_KEY: '<not-a-real-key>',
5354
})
5455

56+
// The provider HTTP client keeps a pooled keep-alive connection whose timer
57+
// captures the async-context frame active when the request ran, so a fixed
58+
// (non-scaling) number of finished spans stays reachable at teardown. Tolerate
59+
// it without loosening the detector for other suites.
60+
withSpanLeakBaseline(20)
61+
5562
withVersions('ai', 'ai', range, (version, _, realVersion) => {
5663
let ai
5764
let openai

‎packages/datadog-plugin-aws-sdk/test/spec_helpers.js‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
'use strict'
22

3+
const { withSpanLeakBaseline } = require('../../dd-trace/test/plugins/span-leak-detector')
34
const { withVersions } = require('../../dd-trace/test/setup/mocha')
45
const { NODE_MAJOR } = require('../../../version')
56

@@ -86,6 +87,12 @@ const helpers = {
8687
withAwsSdkVersions,
8788

8889
setup () {
90+
// The AWS SDK's pooled HTTP connection keeps a keep-alive timer that captures
91+
// the async-context frame active when the request ran, so a fixed (non-scaling)
92+
// number of finished spans stays reachable at teardown. Tolerate it without
93+
// loosening the detector for other suites.
94+
withSpanLeakBaseline(25)
95+
8996
before(() => {
9097
process.env.AWS_SECRET_ACCESS_KEY = '0000000000/00000000000000000000000000000'
9198
process.env.AWS_ACCESS_KEY_ID = '00000000000000000000'

‎packages/datadog-plugin-child_process/test/index.spec.js‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const sinon = require('sinon')
99
const { storage } = require('../../datadog-core')
1010
const agent = require('../../dd-trace/test/plugins/agent')
1111
const { expectSomeSpan } = require('../../dd-trace/test/plugins/helpers')
12+
const { withSpanLeakBaseline } = require('../../dd-trace/test/plugins/span-leak-detector')
1213
const ChildProcessPlugin = require('../src')
1314
const { temporaryWarningExceptions } = require('../../dd-trace/test/setup/core')
1415

@@ -26,6 +27,12 @@ function normalizeArgs (methodName, command, options) {
2627
}
2728

2829
describe('Child process plugin', () => {
30+
// A spawned child process keeps a handle whose timer captures the async-context
31+
// frame active when it was created, so a fixed (non-scaling) number of finished
32+
// spans stays reachable at teardown. Tolerate it without loosening the detector
33+
// for other suites.
34+
withSpanLeakBaseline(25)
35+
2936
describe('unit tests', () => {
3037
let tracerStub, configStub, spanStub
3138

‎packages/datadog-plugin-connect/test/index.spec.js‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const { assertObjectContains } = require('../../../integration-tests/helpers')
1212
const { storage } = require('../../datadog-core')
1313
const { ERROR_MESSAGE, ERROR_STACK, ERROR_TYPE } = require('../../dd-trace/src/constants')
1414
const agent = require('../../dd-trace/test/plugins/agent')
15+
const { withSpanLeakBaseline } = require('../../dd-trace/test/plugins/span-leak-detector')
1516
const { withVersions } = require('../../dd-trace/test/setup/mocha')
1617
const sort = spans => spans.sort((a, b) => a.start.toString() >= b.start.toString() ? 1 : -1)
1718

@@ -20,6 +21,12 @@ describe('Plugin', () => {
2021
let connect
2122
let appListener
2223

24+
// Node's per-connection HTTP keep-alive timer captures the async-context frame
25+
// active when the request ran; the handful of most-recent servers whose timers
26+
// have not fired at teardown keep that fixed (non-scaling) number of finished
27+
// spans reachable. Tolerate it without loosening the detector for other suites.
28+
withSpanLeakBaseline(15)
29+
2330
describe('connect', () => {
2431
withVersions('connect', 'connect', version => {
2532
beforeEach(() => {

‎packages/datadog-plugin-elasticsearch/test/index.spec.js‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ const { breakThen, unbreakThen } = require('../../dd-trace/test/plugins/helpers'
1111
const { withNamingSchema, withPeerService, withVersions } = require('../../dd-trace/test/setup/mocha')
1212
const { assertObjectContains } = require('../../../integration-tests/helpers')
1313
const { temporaryWarningExceptions } = require('../../dd-trace/test/setup/core')
14-
const spanLeakDetector = require('../../dd-trace/test/plugins/span-leak-detector')
14+
const { withSpanLeakBaseline } = require('../../dd-trace/test/plugins/span-leak-detector')
1515
const { expectedSchema, rawExpectedSchema } = require('./naming')
1616
describe('Plugin', () => {
1717
let elasticsearch
@@ -22,8 +22,7 @@ describe('Plugin', () => {
2222
// opened, so a fixed (non-scaling) count of finished spans stays reachable.
2323
// Tolerate that pool-sized retention here without loosening the detector for
2424
// other suites.
25-
before(() => spanLeakDetector.setBaseline(60))
26-
after(() => spanLeakDetector.resetBaseline())
25+
withSpanLeakBaseline(60)
2726

2827
withVersions('elasticsearch', ['elasticsearch', '@elastic/elasticsearch'], (version, moduleName) => {
2928
const metaModule = require(`../../../versions/${moduleName}@${version}`)

‎packages/datadog-plugin-express/test/index.spec.js‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const { NODE_MAJOR } = require('../../../version')
1414
const { storage } = require('../../datadog-core')
1515
const { ERROR_MESSAGE, ERROR_STACK, ERROR_TYPE } = require('../../dd-trace/src/constants')
1616
const agent = require('../../dd-trace/test/plugins/agent')
17+
const { withSpanLeakBaseline } = require('../../dd-trace/test/plugins/span-leak-detector')
1718
const { temporaryWarningExceptions } = require('../../dd-trace/test/setup/core')
1819
const { withVersions } = require('../../dd-trace/test/setup/mocha')
1920
const sort = spans => spans.sort((a, b) => a.start.toString() >= b.start.toString() ? 1 : -1)
@@ -23,6 +24,12 @@ describe('Plugin', () => {
2324
let express
2425
let appListener
2526

27+
// Node's per-connection HTTP keep-alive timer captures the async-context frame
28+
// active when the request ran; the handful of most-recent servers whose timers
29+
// have not fired at teardown keep that fixed (non-scaling) number of finished
30+
// spans reachable. Tolerate it without loosening the detector for other suites.
31+
withSpanLeakBaseline(25)
32+
2633
describe('express', () => {
2734
withVersions('express', 'express', version => {
2835
// Express.js 4.10.5 and below have a Node.js incompatibility in the `fresh` package RE res._headers missing

‎packages/datadog-plugin-grpc/test/client.spec.js‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ const { assertObjectContains } = require('../../../integration-tests/helpers')
1212
const loader = require('../../../versions/@grpc/proto-loader').get()
1313
const { withNamingSchema, withPeerService, withVersions } = require('../../dd-trace/test/setup/mocha')
1414
const agent = require('../../dd-trace/test/plugins/agent')
15-
const spanLeakDetector = require('../../dd-trace/test/plugins/span-leak-detector')
15+
const { withSpanLeakBaseline } = require('../../dd-trace/test/plugins/span-leak-detector')
1616
const { ERROR_MESSAGE, ERROR_TYPE, ERROR_STACK } = require('../../dd-trace/src/constants')
1717
const { defaults } = require('../../dd-trace/src/config/defaults')
1818
const { NODE_MAJOR } = require('../../../version')
@@ -29,8 +29,7 @@ describe('Plugin', () => {
2929
// when the subchannel was built, so a fixed (non-scaling) count of finished
3030
// spans stays reachable. Tolerate that pool-sized retention here without
3131
// loosening the detector for other suites.
32-
before(() => spanLeakDetector.setBaseline(50))
33-
after(() => spanLeakDetector.resetBaseline())
32+
withSpanLeakBaseline(50)
3433

3534
withVersions('grpc', pkgs, NODE_MAJOR >= 25 && '>=1.3.0' || '*', (version, pkg, resolvedVersion) => {
3635
let grpc

‎packages/datadog-plugin-grpc/test/server.spec.js‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const satisfies = require('semifies')
1010
const { assertObjectContains } = require('../../../integration-tests/helpers')
1111
const { withNamingSchema, withVersions } = require('../../dd-trace/test/setup/mocha')
1212
const agent = require('../../dd-trace/test/plugins/agent')
13-
const spanLeakDetector = require('../../dd-trace/test/plugins/span-leak-detector')
13+
const { withSpanLeakBaseline } = require('../../dd-trace/test/plugins/span-leak-detector')
1414
const { ERROR_MESSAGE, ERROR_TYPE, ERROR_STACK } = require('../../dd-trace/src/constants')
1515
const { defaults } = require('../../dd-trace/src/config/defaults')
1616
const { NODE_MAJOR } = require('../../../version')
@@ -26,8 +26,7 @@ describe('Plugin', () => {
2626
// when the subchannel was built, so a fixed (non-scaling) count of finished
2727
// spans stays reachable. Tolerate that pool-sized retention here without
2828
// loosening the detector for other suites.
29-
before(() => spanLeakDetector.setBaseline(50))
30-
after(() => spanLeakDetector.resetBaseline())
29+
withSpanLeakBaseline(50)
3130

3231
withVersions('grpc', pkgs, NODE_MAJOR >= 25 ? '>=1.3.0' : '*', (version, pkg, resolvedVersion) => {
3332
let grpc

‎packages/datadog-plugin-koa/test/index.spec.js‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const { assertObjectContains } = require('../../../integration-tests/helpers')
1414
const { storage } = require('../../datadog-core')
1515
const { ERROR_TYPE } = require('../../dd-trace/src/constants')
1616
const agent = require('../../dd-trace/test/plugins/agent')
17+
const { withSpanLeakBaseline } = require('../../dd-trace/test/plugins/span-leak-detector')
1718
const { withVersions } = require('../../dd-trace/test/setup/mocha')
1819

1920
const sort = spans => spans.sort((a, b) => a.start.toString() >= b.start.toString() ? 1 : -1)
@@ -23,6 +24,12 @@ describe('Plugin', () => {
2324
let Koa
2425
let appListener
2526

27+
// Node's per-connection HTTP keep-alive timer captures the async-context frame
28+
// active when the request ran; the handful of most-recent servers whose timers
29+
// have not fired at teardown keep that fixed (non-scaling) number of finished
30+
// spans reachable. Tolerate it without loosening the detector for other suites.
31+
withSpanLeakBaseline(15)
32+
2633
describe('koa', () => {
2734
withVersions('koa', 'koa', (version, _, realVersion) => {
2835
beforeEach(() => {

‎packages/datadog-plugin-langchain/test/index.spec.js‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,18 @@ const semifies = require('semifies')
99
const { assertObjectContains, useEnv } = require('../../../integration-tests/helpers')
1010
const iastFilter = require('../../dd-trace/src/appsec/iast/taint-tracking/filter')
1111
const agent = require('../../dd-trace/test/plugins/agent')
12+
const { withSpanLeakBaseline } = require('../../dd-trace/test/plugins/span-leak-detector')
1213
const { withVersions } = require('../../dd-trace/test/setup/mocha')
1314

1415
const isDdTrace = iastFilter.isDdTrace
1516

1617
describe('Plugin', () => {
18+
// The provider HTTP client keeps a pooled keep-alive connection whose timer
19+
// captures the async-context frame active when the request ran, so a fixed
20+
// (non-scaling) number of finished spans stays reachable at teardown. Tolerate
21+
// it without loosening the detector for other suites.
22+
withSpanLeakBaseline(20)
23+
1724
let langchainOpenai
1825
let langchainAnthropic
1926
let langchainGoogleGenAI

0 commit comments

Comments
 (0)