dd-trace 5.94.0 → 5.95.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE-3rdparty.csv +46 -44
- package/index.d.ts +182 -13
- package/package.json +1 -1
- package/packages/datadog-instrumentations/src/anthropic.js +1 -1
- package/packages/datadog-instrumentations/src/helpers/rewriter/{orchestrion/compiler.js → compiler.js} +4 -13
- package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +16 -2
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/langgraph.js +2 -2
- package/packages/datadog-instrumentations/src/helpers/rewriter/{orchestrion/transforms.js → transforms.js} +3 -89
- package/packages/datadog-plugin-dd-trace-api/src/index.js +1 -4
- package/packages/dd-trace/src/azure_metadata.js +15 -15
- package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +73 -1
- package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +76 -1
- package/packages/dd-trace/src/ci-visibility/requests/fs-cache.js +259 -0
- package/packages/dd-trace/src/ci-visibility/test-management/get-test-management-tests.js +56 -0
- package/packages/dd-trace/src/config/config-base.d.ts +7 -0
- package/packages/dd-trace/src/config/config-base.js +5 -0
- package/packages/dd-trace/src/config/config-types.d.ts +78 -0
- package/packages/dd-trace/src/config/generated-config-types.d.ts +582 -0
- package/packages/dd-trace/src/config/supported-configurations.json +7 -0
- package/packages/dd-trace/src/llmobs/constants/tags.js +1 -0
- package/packages/dd-trace/src/llmobs/constants/text.js +1 -1
- package/packages/dd-trace/src/llmobs/constants/writers.js +1 -1
- package/packages/dd-trace/src/llmobs/plugins/anthropic.js +11 -2
- package/packages/dd-trace/src/llmobs/plugins/openai/index.js +4 -1
- package/packages/dd-trace/src/llmobs/writers/spans.js +1 -1
- package/packages/dd-trace/src/priority_sampler.js +1 -1
- package/packages/dd-trace/src/rate_limiter.js +2 -1
- package/packages/dd-trace/src/tagger.js +31 -35
- package/vendor/dist/@apm-js-collab/code-transformer/LICENSE +28 -0
- package/vendor/dist/@apm-js-collab/code-transformer/index.js +133 -0
- package/vendor/dist/@opentelemetry/core/index.js +1 -1
- package/vendor/dist/@opentelemetry/resources/index.js +1 -1
- package/vendor/dist/esquery/index.js +1 -1
- package/vendor/dist/meriyah/index.js +1 -1
- package/packages/datadog-instrumentations/src/helpers/rewriter/orchestrion/index.js +0 -43
- package/packages/datadog-instrumentations/src/helpers/rewriter/orchestrion/matcher.js +0 -49
- package/packages/datadog-instrumentations/src/helpers/rewriter/orchestrion/transformer.js +0 -121
- package/vendor/dist/astring/LICENSE +0 -19
- package/vendor/dist/astring/index.js +0 -1
|
@@ -3,14 +3,10 @@
|
|
|
3
3
|
const Plugin = require('../../dd-trace/src/plugins/plugin')
|
|
4
4
|
const telemetryMetrics = require('../../dd-trace/src/telemetry/metrics')
|
|
5
5
|
const apiMetrics = telemetryMetrics.manager.namespace('tracers')
|
|
6
|
-
const { getValueFromEnvSources } = require('../../dd-trace/src/config/helper')
|
|
7
6
|
|
|
8
7
|
// api ==> here
|
|
9
8
|
const objectMap = new WeakMap()
|
|
10
9
|
|
|
11
|
-
const injectionEnabledTag =
|
|
12
|
-
`injection_enabled:${getValueFromEnvSources('DD_INJECTION_ENABLED') ? 'yes' : 'no'}`
|
|
13
|
-
|
|
14
10
|
module.exports = class DdTraceApiPlugin extends Plugin {
|
|
15
11
|
static id = 'dd-trace-api'
|
|
16
12
|
|
|
@@ -18,6 +14,7 @@ module.exports = class DdTraceApiPlugin extends Plugin {
|
|
|
18
14
|
super(...args)
|
|
19
15
|
|
|
20
16
|
const tracer = this._tracer
|
|
17
|
+
const injectionEnabledTag = `injection_enabled:${this._tracerConfig.injectionEnabled ? 'yes' : 'no'}`
|
|
21
18
|
|
|
22
19
|
this.addSub('datadog-api:v1:tracerinit', ({ proxy }) => {
|
|
23
20
|
const proxyVal = proxy()
|
|
@@ -5,10 +5,9 @@
|
|
|
5
5
|
const os = require('os')
|
|
6
6
|
const {
|
|
7
7
|
getEnvironmentVariable,
|
|
8
|
-
getEnvironmentVariables,
|
|
9
8
|
getValueFromEnvSources,
|
|
10
9
|
} = require('./config/helper')
|
|
11
|
-
const { getIsAzureFunction
|
|
10
|
+
const { getIsAzureFunction } = require('./serverless')
|
|
12
11
|
|
|
13
12
|
function extractSubscriptionID (ownerName) {
|
|
14
13
|
if (ownerName !== undefined) {
|
|
@@ -46,32 +45,33 @@ function trimObject (obj) {
|
|
|
46
45
|
}
|
|
47
46
|
|
|
48
47
|
function buildMetadata () {
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
} = getEnvironmentVariables()
|
|
48
|
+
const COMPUTERNAME = getEnvironmentVariable('COMPUTERNAME')
|
|
49
|
+
const FUNCTIONS_EXTENSION_VERSION = getEnvironmentVariable('FUNCTIONS_EXTENSION_VERSION')
|
|
50
|
+
const FUNCTIONS_WORKER_RUNTIME = getEnvironmentVariable('FUNCTIONS_WORKER_RUNTIME')
|
|
51
|
+
const FUNCTIONS_WORKER_RUNTIME_VERSION = getEnvironmentVariable('FUNCTIONS_WORKER_RUNTIME_VERSION')
|
|
52
|
+
const WEBSITE_INSTANCE_ID = getEnvironmentVariable('WEBSITE_INSTANCE_ID')
|
|
53
|
+
const WEBSITE_OWNER_NAME = getEnvironmentVariable('WEBSITE_OWNER_NAME')
|
|
54
|
+
const WEBSITE_OS = getEnvironmentVariable('WEBSITE_OS')
|
|
55
|
+
const WEBSITE_RESOURCE_GROUP = getEnvironmentVariable('WEBSITE_RESOURCE_GROUP')
|
|
56
|
+
const WEBSITE_SITE_NAME = getEnvironmentVariable('WEBSITE_SITE_NAME')
|
|
57
|
+
const WEBSITE_SKU = getEnvironmentVariable('WEBSITE_SKU')
|
|
60
58
|
|
|
61
59
|
const DD_AZURE_RESOURCE_GROUP = getValueFromEnvSources('DD_AZURE_RESOURCE_GROUP')
|
|
60
|
+
const isAzureFunction = FUNCTIONS_EXTENSION_VERSION !== undefined && FUNCTIONS_WORKER_RUNTIME !== undefined
|
|
61
|
+
const isFlexConsumptionAzureFunction = isAzureFunction && WEBSITE_SKU === 'FlexConsumption'
|
|
62
62
|
|
|
63
63
|
const subscriptionID = extractSubscriptionID(WEBSITE_OWNER_NAME)
|
|
64
64
|
|
|
65
65
|
const siteName = WEBSITE_SITE_NAME
|
|
66
66
|
|
|
67
|
-
const [siteKind, siteType] =
|
|
67
|
+
const [siteKind, siteType] = isAzureFunction
|
|
68
68
|
? ['functionapp', 'function']
|
|
69
69
|
: ['app', 'app']
|
|
70
70
|
|
|
71
71
|
// Azure Functions on Flex Consumption plans require the `DD_AZURE_RESOURCE_GROUP` env var.
|
|
72
72
|
// If this logic ever changes, update the logic in `libdatadog`, `serverless-components/src/datadog-trace-agent`,
|
|
73
73
|
// and the serverless compat layers accordingly.
|
|
74
|
-
const resourceGroup =
|
|
74
|
+
const resourceGroup = isFlexConsumptionAzureFunction
|
|
75
75
|
? (DD_AZURE_RESOURCE_GROUP ?? WEBSITE_RESOURCE_GROUP ?? extractResourceGroup(WEBSITE_OWNER_NAME))
|
|
76
76
|
: (WEBSITE_RESOURCE_GROUP ?? extractResourceGroup(WEBSITE_OWNER_NAME))
|
|
77
77
|
|
|
@@ -16,12 +16,17 @@ const {
|
|
|
16
16
|
} = require('../../ci-visibility/telemetry')
|
|
17
17
|
|
|
18
18
|
const { getNumFromKnownTests } = require('../../plugins/util/test')
|
|
19
|
+
const { buildCacheKey, writeToCache, withCache } = require('../requests/fs-cache')
|
|
19
20
|
|
|
20
21
|
const MAX_KNOWN_TESTS_PAGES = 10_000
|
|
21
22
|
|
|
22
23
|
/**
|
|
23
24
|
* Deep-merges page tests into aggregate.
|
|
24
25
|
* Structure: { module: { suite: [testName, ...] } }
|
|
26
|
+
*
|
|
27
|
+
* @param {object | null} aggregate
|
|
28
|
+
* @param {object | null} page
|
|
29
|
+
* @returns {object | null}
|
|
25
30
|
*/
|
|
26
31
|
function mergeKnownTests (aggregate, page) {
|
|
27
32
|
if (!page) return aggregate
|
|
@@ -62,6 +67,71 @@ function getKnownTests ({
|
|
|
62
67
|
runtimeName,
|
|
63
68
|
runtimeVersion,
|
|
64
69
|
custom,
|
|
70
|
+
}, done) {
|
|
71
|
+
const cacheKey = buildCacheKey('known-tests', [
|
|
72
|
+
sha, service, env, repositoryUrl, osPlatform, osVersion, osArchitecture,
|
|
73
|
+
runtimeName, runtimeVersion, custom,
|
|
74
|
+
])
|
|
75
|
+
|
|
76
|
+
withCache(cacheKey, (activeCacheKey, cb) => {
|
|
77
|
+
fetchFromApi({
|
|
78
|
+
url,
|
|
79
|
+
isEvpProxy,
|
|
80
|
+
evpProxyPrefix,
|
|
81
|
+
isGzipCompatible,
|
|
82
|
+
env,
|
|
83
|
+
service,
|
|
84
|
+
repositoryUrl,
|
|
85
|
+
sha,
|
|
86
|
+
osVersion,
|
|
87
|
+
osPlatform,
|
|
88
|
+
osArchitecture,
|
|
89
|
+
runtimeName,
|
|
90
|
+
runtimeVersion,
|
|
91
|
+
custom,
|
|
92
|
+
cacheKey: activeCacheKey,
|
|
93
|
+
}, cb)
|
|
94
|
+
}, done)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Fetches known tests from the API with cursor-based pagination and writes the
|
|
99
|
+
* result to cache on success.
|
|
100
|
+
*
|
|
101
|
+
* @param {object} params
|
|
102
|
+
* @param {string} params.url
|
|
103
|
+
* @param {boolean} params.isEvpProxy
|
|
104
|
+
* @param {string} params.evpProxyPrefix
|
|
105
|
+
* @param {boolean} params.isGzipCompatible
|
|
106
|
+
* @param {string} params.env
|
|
107
|
+
* @param {string} params.service
|
|
108
|
+
* @param {string} params.repositoryUrl
|
|
109
|
+
* @param {string} params.sha
|
|
110
|
+
* @param {string} params.osVersion
|
|
111
|
+
* @param {string} params.osPlatform
|
|
112
|
+
* @param {string} params.osArchitecture
|
|
113
|
+
* @param {string} params.runtimeName
|
|
114
|
+
* @param {string} params.runtimeVersion
|
|
115
|
+
* @param {object} [params.custom]
|
|
116
|
+
* @param {string | null} params.cacheKey
|
|
117
|
+
* @param {Function} done
|
|
118
|
+
*/
|
|
119
|
+
function fetchFromApi ({
|
|
120
|
+
url,
|
|
121
|
+
isEvpProxy,
|
|
122
|
+
evpProxyPrefix,
|
|
123
|
+
isGzipCompatible,
|
|
124
|
+
env,
|
|
125
|
+
service,
|
|
126
|
+
repositoryUrl,
|
|
127
|
+
sha,
|
|
128
|
+
osVersion,
|
|
129
|
+
osPlatform,
|
|
130
|
+
osArchitecture,
|
|
131
|
+
runtimeName,
|
|
132
|
+
runtimeVersion,
|
|
133
|
+
custom,
|
|
134
|
+
cacheKey,
|
|
65
135
|
}, done) {
|
|
66
136
|
const options = {
|
|
67
137
|
path: '/api/v2/ci/libraries/tests',
|
|
@@ -166,7 +236,9 @@ function getKnownTests ({
|
|
|
166
236
|
distributionMetric(TELEMETRY_KNOWN_TESTS_RESPONSE_TESTS, {}, numTests)
|
|
167
237
|
distributionMetric(TELEMETRY_KNOWN_TESTS_RESPONSE_BYTES, {}, totalResponseBytes)
|
|
168
238
|
|
|
169
|
-
log.debug('Number of received known tests:', numTests)
|
|
239
|
+
log.debug('Number of received known tests: %d', numTests)
|
|
240
|
+
|
|
241
|
+
writeToCache(cacheKey, aggregateTests)
|
|
170
242
|
|
|
171
243
|
done(null, aggregateTests)
|
|
172
244
|
} catch (err) {
|
|
@@ -13,6 +13,7 @@ const {
|
|
|
13
13
|
TELEMETRY_ITR_SKIPPABLE_TESTS_RESPONSE_TESTS,
|
|
14
14
|
TELEMETRY_ITR_SKIPPABLE_TESTS_RESPONSE_BYTES,
|
|
15
15
|
} = require('../../ci-visibility/telemetry')
|
|
16
|
+
const { buildCacheKey, writeToCache, withCache } = require('../requests/fs-cache')
|
|
16
17
|
|
|
17
18
|
function getSkippableSuites ({
|
|
18
19
|
url,
|
|
@@ -30,6 +31,76 @@ function getSkippableSuites ({
|
|
|
30
31
|
runtimeVersion,
|
|
31
32
|
custom,
|
|
32
33
|
testLevel = 'suite',
|
|
34
|
+
}, done) {
|
|
35
|
+
const cacheKey = buildCacheKey('skippable', [
|
|
36
|
+
sha, service, env, repositoryUrl, osPlatform, osVersion, osArchitecture,
|
|
37
|
+
runtimeName, runtimeVersion, testLevel, custom,
|
|
38
|
+
])
|
|
39
|
+
|
|
40
|
+
withCache(cacheKey, (activeCacheKey, cb) => {
|
|
41
|
+
fetchFromApi({
|
|
42
|
+
url,
|
|
43
|
+
isEvpProxy,
|
|
44
|
+
evpProxyPrefix,
|
|
45
|
+
isGzipCompatible,
|
|
46
|
+
env,
|
|
47
|
+
service,
|
|
48
|
+
repositoryUrl,
|
|
49
|
+
sha,
|
|
50
|
+
osVersion,
|
|
51
|
+
osPlatform,
|
|
52
|
+
osArchitecture,
|
|
53
|
+
runtimeName,
|
|
54
|
+
runtimeVersion,
|
|
55
|
+
custom,
|
|
56
|
+
testLevel,
|
|
57
|
+
cacheKey: activeCacheKey,
|
|
58
|
+
}, cb)
|
|
59
|
+
}, (err, data) => {
|
|
60
|
+
if (err) return done(err)
|
|
61
|
+
done(null, data.skippableSuites, data.correlationId)
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Fetches skippable suites from the API and writes the result to cache on success.
|
|
67
|
+
*
|
|
68
|
+
* @param {object} params
|
|
69
|
+
* @param {string} params.url
|
|
70
|
+
* @param {boolean} params.isEvpProxy
|
|
71
|
+
* @param {string} params.evpProxyPrefix
|
|
72
|
+
* @param {boolean} params.isGzipCompatible
|
|
73
|
+
* @param {string} params.env
|
|
74
|
+
* @param {string} params.service
|
|
75
|
+
* @param {string} params.repositoryUrl
|
|
76
|
+
* @param {string} params.sha
|
|
77
|
+
* @param {string} params.osVersion
|
|
78
|
+
* @param {string} params.osPlatform
|
|
79
|
+
* @param {string} params.osArchitecture
|
|
80
|
+
* @param {string} params.runtimeName
|
|
81
|
+
* @param {string} params.runtimeVersion
|
|
82
|
+
* @param {object} [params.custom]
|
|
83
|
+
* @param {string} [params.testLevel]
|
|
84
|
+
* @param {string | null} params.cacheKey
|
|
85
|
+
* @param {Function} done
|
|
86
|
+
*/
|
|
87
|
+
function fetchFromApi ({
|
|
88
|
+
url,
|
|
89
|
+
isEvpProxy,
|
|
90
|
+
evpProxyPrefix,
|
|
91
|
+
isGzipCompatible,
|
|
92
|
+
env,
|
|
93
|
+
service,
|
|
94
|
+
repositoryUrl,
|
|
95
|
+
sha,
|
|
96
|
+
osVersion,
|
|
97
|
+
osPlatform,
|
|
98
|
+
osArchitecture,
|
|
99
|
+
runtimeName,
|
|
100
|
+
runtimeVersion,
|
|
101
|
+
custom,
|
|
102
|
+
testLevel,
|
|
103
|
+
cacheKey,
|
|
33
104
|
}, done) {
|
|
34
105
|
const options = {
|
|
35
106
|
path: '/api/v2/ci/tests/skippable',
|
|
@@ -109,7 +180,11 @@ function getSkippableSuites ({
|
|
|
109
180
|
)
|
|
110
181
|
distributionMetric(TELEMETRY_ITR_SKIPPABLE_TESTS_RESPONSE_BYTES, {}, res.length)
|
|
111
182
|
log.debug('Number of received skippable %ss:', testLevel, skippableSuites.length)
|
|
112
|
-
|
|
183
|
+
|
|
184
|
+
const result = { skippableSuites, correlationId }
|
|
185
|
+
writeToCache(cacheKey, result)
|
|
186
|
+
|
|
187
|
+
done(null, result)
|
|
113
188
|
} catch (err) {
|
|
114
189
|
done(err)
|
|
115
190
|
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs')
|
|
4
|
+
const path = require('node:path')
|
|
5
|
+
const { createHash } = require('node:crypto')
|
|
6
|
+
const { tmpdir } = require('node:os')
|
|
7
|
+
|
|
8
|
+
const log = require('../../log')
|
|
9
|
+
const { getValueFromEnvSources } = require('../../config/helper')
|
|
10
|
+
|
|
11
|
+
const CACHE_TTL_MS = 30 * 60 * 1000 // 30 minutes
|
|
12
|
+
const CACHE_LOCK_POLL_MS = 500
|
|
13
|
+
const CACHE_LOCK_TIMEOUT_MS = 120_000 // 2 minutes
|
|
14
|
+
const CACHE_LOCK_HEARTBEAT_MS = 30_000 // 30 seconds
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Returns whether the filesystem cache is enabled via the env var.
|
|
18
|
+
*
|
|
19
|
+
* @returns {boolean}
|
|
20
|
+
*/
|
|
21
|
+
function isCacheEnabled () {
|
|
22
|
+
const { isTrue } = require('../../util')
|
|
23
|
+
return isTrue(getValueFromEnvSources('DD_EXPERIMENTAL_TEST_REQUESTS_FS_CACHE'))
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Builds a deterministic cache key by hashing arbitrary key-value parts.
|
|
28
|
+
*
|
|
29
|
+
* @param {string} prefix - Cache file prefix (e.g. 'known-tests', 'skippable', 'test-mgmt')
|
|
30
|
+
* @param {Array<unknown>} parts - Values that uniquely identify the cached response
|
|
31
|
+
* @returns {string}
|
|
32
|
+
*/
|
|
33
|
+
function buildCacheKey (prefix, parts) {
|
|
34
|
+
const hash = createHash('sha256').update(JSON.stringify(parts)).digest('hex').slice(0, 16)
|
|
35
|
+
return `${prefix}-${hash}`
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @param {string} cacheKey
|
|
40
|
+
* @returns {string}
|
|
41
|
+
*/
|
|
42
|
+
function getCachePath (cacheKey) {
|
|
43
|
+
return path.join(tmpdir(), `dd-${cacheKey}.json`)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @param {string} cacheKey
|
|
48
|
+
* @returns {string}
|
|
49
|
+
*/
|
|
50
|
+
function getLockPath (cacheKey) {
|
|
51
|
+
return path.join(tmpdir(), `dd-${cacheKey}.lock`)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Attempts to read cached data from the filesystem.
|
|
56
|
+
*
|
|
57
|
+
* @param {string} cacheKey
|
|
58
|
+
* @returns {{ data: unknown } | undefined}
|
|
59
|
+
*/
|
|
60
|
+
function readFromCache (cacheKey) {
|
|
61
|
+
const cachePath = getCachePath(cacheKey)
|
|
62
|
+
try {
|
|
63
|
+
const raw = fs.readFileSync(cachePath, 'utf8')
|
|
64
|
+
const parsed = JSON.parse(raw)
|
|
65
|
+
if (!Object.hasOwn(parsed, 'data')) {
|
|
66
|
+
log.debug('%s cache file has no data field, ignoring', cacheKey)
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
const { timestamp, data } = parsed
|
|
70
|
+
if (Date.now() - timestamp > CACHE_TTL_MS) {
|
|
71
|
+
log.debug('%s cache expired (age: %d ms)', cacheKey, Date.now() - timestamp)
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
log.debug('%s cache hit', cacheKey)
|
|
75
|
+
return { data }
|
|
76
|
+
} catch {
|
|
77
|
+
// Cache file missing, corrupt, or unreadable — treat as cache miss
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Writes data to the filesystem cache atomically.
|
|
83
|
+
*
|
|
84
|
+
* @param {string} cacheKey
|
|
85
|
+
* @param {unknown} data
|
|
86
|
+
*/
|
|
87
|
+
function writeToCache (cacheKey, data) {
|
|
88
|
+
if (!cacheKey) return
|
|
89
|
+
const cachePath = getCachePath(cacheKey)
|
|
90
|
+
const tmpPath = cachePath + '.tmp.' + process.pid
|
|
91
|
+
try {
|
|
92
|
+
fs.writeFileSync(tmpPath, JSON.stringify({ timestamp: Date.now(), data }), 'utf8')
|
|
93
|
+
fs.renameSync(tmpPath, cachePath)
|
|
94
|
+
log.debug('Cache written: %s', cachePath)
|
|
95
|
+
} catch (err) {
|
|
96
|
+
log.error('Failed to write cache %s: %s', cacheKey, err.message)
|
|
97
|
+
try { fs.unlinkSync(tmpPath) } catch { /* ignore */ }
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Attempts to acquire an exclusive lock using O_CREAT|O_EXCL.
|
|
103
|
+
*
|
|
104
|
+
* @param {string} cacheKey
|
|
105
|
+
* @returns {boolean}
|
|
106
|
+
*/
|
|
107
|
+
function tryAcquireLock (cacheKey) {
|
|
108
|
+
const lockPath = getLockPath(cacheKey)
|
|
109
|
+
try {
|
|
110
|
+
const fd = fs.openSync(lockPath, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY)
|
|
111
|
+
fs.writeSync(fd, String(Date.now()))
|
|
112
|
+
fs.closeSync(fd)
|
|
113
|
+
return true
|
|
114
|
+
} catch {
|
|
115
|
+
return false
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Removes the lock file.
|
|
121
|
+
*
|
|
122
|
+
* @param {string} cacheKey
|
|
123
|
+
*/
|
|
124
|
+
function releaseLock (cacheKey) {
|
|
125
|
+
try { fs.unlinkSync(getLockPath(cacheKey)) } catch { /* ignore */ }
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Updates the lock file timestamp so waiters know the owner is still alive.
|
|
130
|
+
*
|
|
131
|
+
* @param {string} cacheKey
|
|
132
|
+
*/
|
|
133
|
+
function touchLock (cacheKey) {
|
|
134
|
+
const lockPath = getLockPath(cacheKey)
|
|
135
|
+
const tmpPath = lockPath + '.tmp.' + process.pid
|
|
136
|
+
try {
|
|
137
|
+
fs.writeFileSync(tmpPath, String(Date.now()))
|
|
138
|
+
fs.renameSync(tmpPath, lockPath)
|
|
139
|
+
} catch {
|
|
140
|
+
try { fs.unlinkSync(tmpPath) } catch { /* ignore */ }
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Starts a periodic heartbeat that touches the lock file.
|
|
146
|
+
* Returns a function that stops the heartbeat and releases the lock.
|
|
147
|
+
*
|
|
148
|
+
* @param {string} cacheKey
|
|
149
|
+
* @returns {Function}
|
|
150
|
+
*/
|
|
151
|
+
function startLockHeartbeat (cacheKey) {
|
|
152
|
+
const interval = setInterval(() => touchLock(cacheKey), CACHE_LOCK_HEARTBEAT_MS)
|
|
153
|
+
interval.unref()
|
|
154
|
+
return () => {
|
|
155
|
+
clearInterval(interval)
|
|
156
|
+
try { fs.unlinkSync(getLockPath(cacheKey)) } catch { /* ignore */ }
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Checks whether the lock file is stale (older than the lock timeout).
|
|
162
|
+
*
|
|
163
|
+
* @param {string} cacheKey
|
|
164
|
+
* @returns {boolean}
|
|
165
|
+
*/
|
|
166
|
+
function isLockStale (cacheKey) {
|
|
167
|
+
try {
|
|
168
|
+
const content = fs.readFileSync(getLockPath(cacheKey), 'utf8')
|
|
169
|
+
return Date.now() - Number(content) > CACHE_LOCK_TIMEOUT_MS
|
|
170
|
+
} catch {
|
|
171
|
+
return true
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Polls until the cache file appears or the timeout is reached.
|
|
177
|
+
*
|
|
178
|
+
* @param {string} cacheKey
|
|
179
|
+
* @param {Function} fetchFn - function(done) that fetches from the API
|
|
180
|
+
* @param {Function} done - callback(err, ...results)
|
|
181
|
+
*/
|
|
182
|
+
function waitForCache (cacheKey, fetchFn, done) {
|
|
183
|
+
const poll = () => {
|
|
184
|
+
const cached = readFromCache(cacheKey)
|
|
185
|
+
if (cached) {
|
|
186
|
+
return done(null, cached.data)
|
|
187
|
+
}
|
|
188
|
+
if (isLockStale(cacheKey)) {
|
|
189
|
+
log.debug('%s lock is stale, attempting takeover', cacheKey)
|
|
190
|
+
releaseLock(cacheKey)
|
|
191
|
+
if (!tryAcquireLock(cacheKey)) {
|
|
192
|
+
return setTimeout(poll, CACHE_LOCK_POLL_MS)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const cachedAfterTakeover = readFromCache(cacheKey)
|
|
196
|
+
if (cachedAfterTakeover) {
|
|
197
|
+
releaseLock(cacheKey)
|
|
198
|
+
return done(null, cachedAfterTakeover.data)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const stopHeartbeat = startLockHeartbeat(cacheKey)
|
|
202
|
+
return fetchFn((err, ...results) => {
|
|
203
|
+
stopHeartbeat()
|
|
204
|
+
done(err, ...results)
|
|
205
|
+
})
|
|
206
|
+
}
|
|
207
|
+
setTimeout(poll, CACHE_LOCK_POLL_MS)
|
|
208
|
+
}
|
|
209
|
+
poll()
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Wraps a fetch function with filesystem-based caching and cross-process deduplication.
|
|
214
|
+
*
|
|
215
|
+
* When cache is disabled (env var not set), calls fetchFn directly.
|
|
216
|
+
* When enabled, checks cache → acquires lock → fetches → writes cache → releases lock.
|
|
217
|
+
*
|
|
218
|
+
* @param {string} cacheKey - Unique cache key for this request
|
|
219
|
+
* @param {Function} fetchFn - function(cacheKey, done) that performs the API request.
|
|
220
|
+
* Must call writeToCache(cacheKey, data) on success before calling done(null, data).
|
|
221
|
+
* @param {Function} done - callback(err, ...results)
|
|
222
|
+
*/
|
|
223
|
+
function withCache (cacheKey, fetchFn, done) {
|
|
224
|
+
if (!isCacheEnabled()) {
|
|
225
|
+
return fetchFn(null, done)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Fast path: cache hit
|
|
229
|
+
const cached = readFromCache(cacheKey)
|
|
230
|
+
if (cached) {
|
|
231
|
+
return done(null, cached.data)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Try to become the fetcher (lock owner)
|
|
235
|
+
const isLockOwner = tryAcquireLock(cacheKey)
|
|
236
|
+
|
|
237
|
+
if (!isLockOwner) {
|
|
238
|
+
log.debug('%s lock held by another process, waiting for cache', cacheKey)
|
|
239
|
+
return waitForCache(cacheKey, (cb) => fetchFn(cacheKey, cb), done)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// This process owns the lock — start heartbeat and fetch
|
|
243
|
+
const stopHeartbeat = startLockHeartbeat(cacheKey)
|
|
244
|
+
|
|
245
|
+
fetchFn(cacheKey, (err, ...results) => {
|
|
246
|
+
stopHeartbeat()
|
|
247
|
+
done(err, ...results)
|
|
248
|
+
})
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
module.exports = {
|
|
252
|
+
isCacheEnabled,
|
|
253
|
+
buildCacheKey,
|
|
254
|
+
readFromCache,
|
|
255
|
+
writeToCache,
|
|
256
|
+
withCache,
|
|
257
|
+
getCachePath,
|
|
258
|
+
getLockPath,
|
|
259
|
+
}
|
|
@@ -15,6 +15,8 @@ const {
|
|
|
15
15
|
TELEMETRY_TEST_MANAGEMENT_TESTS_RESPONSE_BYTES,
|
|
16
16
|
} = require('../telemetry')
|
|
17
17
|
|
|
18
|
+
const { buildCacheKey, writeToCache, withCache } = require('../requests/fs-cache')
|
|
19
|
+
|
|
18
20
|
// Calculate the number of tests from the test management tests response, which has a shape like:
|
|
19
21
|
// { module: { suites: { suite: { tests: { testName: { properties: {...} } } } } } }
|
|
20
22
|
function getNumFromTestManagementTests (testManagementTests) {
|
|
@@ -48,6 +50,58 @@ function getTestManagementTests ({
|
|
|
48
50
|
commitHeadSha,
|
|
49
51
|
commitHeadMessage,
|
|
50
52
|
branch,
|
|
53
|
+
}, done) {
|
|
54
|
+
const effectiveSha = commitHeadSha || sha
|
|
55
|
+
const cacheKey = buildCacheKey('test-mgmt', [
|
|
56
|
+
effectiveSha, repositoryUrl, branch,
|
|
57
|
+
])
|
|
58
|
+
|
|
59
|
+
withCache(cacheKey, (activeCacheKey, cb) => {
|
|
60
|
+
fetchFromApi({
|
|
61
|
+
url,
|
|
62
|
+
isEvpProxy,
|
|
63
|
+
evpProxyPrefix,
|
|
64
|
+
isGzipCompatible,
|
|
65
|
+
repositoryUrl,
|
|
66
|
+
commitMessage,
|
|
67
|
+
sha,
|
|
68
|
+
commitHeadSha,
|
|
69
|
+
commitHeadMessage,
|
|
70
|
+
branch,
|
|
71
|
+
cacheKey: activeCacheKey,
|
|
72
|
+
}, cb)
|
|
73
|
+
}, done)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Fetches test management tests from the API and writes the result to cache on success.
|
|
78
|
+
*
|
|
79
|
+
* @param {object} params
|
|
80
|
+
* @param {string} params.url
|
|
81
|
+
* @param {boolean} params.isEvpProxy
|
|
82
|
+
* @param {string} params.evpProxyPrefix
|
|
83
|
+
* @param {boolean} params.isGzipCompatible
|
|
84
|
+
* @param {string} params.repositoryUrl
|
|
85
|
+
* @param {string} [params.commitMessage]
|
|
86
|
+
* @param {string} params.sha
|
|
87
|
+
* @param {string} [params.commitHeadSha]
|
|
88
|
+
* @param {string} [params.commitHeadMessage]
|
|
89
|
+
* @param {string} [params.branch]
|
|
90
|
+
* @param {string | null} params.cacheKey
|
|
91
|
+
* @param {Function} done
|
|
92
|
+
*/
|
|
93
|
+
function fetchFromApi ({
|
|
94
|
+
url,
|
|
95
|
+
isEvpProxy,
|
|
96
|
+
evpProxyPrefix,
|
|
97
|
+
isGzipCompatible,
|
|
98
|
+
repositoryUrl,
|
|
99
|
+
commitMessage,
|
|
100
|
+
sha,
|
|
101
|
+
commitHeadSha,
|
|
102
|
+
commitHeadMessage,
|
|
103
|
+
branch,
|
|
104
|
+
cacheKey,
|
|
51
105
|
}, done) {
|
|
52
106
|
const options = {
|
|
53
107
|
path: '/api/v2/test/libraries/test-management/tests',
|
|
@@ -110,6 +164,8 @@ function getTestManagementTests ({
|
|
|
110
164
|
|
|
111
165
|
log.debug('Test management tests received: %j', testManagementTests)
|
|
112
166
|
|
|
167
|
+
writeToCache(cacheKey, testManagementTests)
|
|
168
|
+
|
|
113
169
|
done(null, testManagementTests)
|
|
114
170
|
} catch (err) {
|
|
115
171
|
done(err)
|