Skip to content

Commit 7aae4fc

Browse files
authored
[DI] Remove source map cache in Node.js 18 (#5302)
* [DI] Remove source map cache for Node.js 18 This is the second attempt at a workaround for a bug in Node.js core, that sometimes triggers when, among other things, a timer is created. This makes the call to `setTimeout` throw an un-catchable error from within `AsyncLocalStorage._propagate` with the following TypeError: Cannot read properties of undefined (reading 'Symbol(kResourceStore)') * Adjust cache timings
1 parent f07a03a commit 7aae4fc

File tree

2 files changed

+83
-24
lines changed

2 files changed

+83
-24
lines changed

packages/dd-trace/src/debugger/devtools_client/source-maps.js

+22-17
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@ const { join, dirname } = require('path')
44
const { readFileSync } = require('fs')
55
const { readFile } = require('fs/promises')
66
const { SourceMapConsumer } = require('source-map')
7+
const { NODE_MAJOR } = require('../../../../../version')
78

89
const cache = new Map()
910
let cacheTimer = null
10-
let cacheTimerLastSet = 0
11+
let cacheTime = null
1112

1213
const self = module.exports = {
1314
async loadSourceMap (dir, url) {
@@ -34,28 +35,32 @@ const self = module.exports = {
3435
}
3536
}
3637

37-
// TODO: Remove if-statement around `setTimeout` below once it's safe to do so.
38-
//
39-
// This is a workaround for, what seems like a bug in Node.js core, that seems to trigger when, among other things, a
40-
// lot of timers are being created very rapidly. This makes the call to `setTimeout` throw an error from within
41-
// `AsyncLocalStorage._propagate` with the following error message:
38+
// The version check inside this function is to guard against a bug Node.js version 18, in which calls to `setTimeout`
39+
// might throw an uncatchable error from within `AsyncLocalStorage._propagate` with the following error message:
4240
//
4341
// TypeError: Cannot read properties of undefined (reading 'Symbol(kResourceStore)')
44-
//
45-
// Source: https://github.com/nodejs/node/blob/v18.20.6/lib/async_hooks.js#L312
4642
function cacheIt (key, value) {
47-
const now = Date.now()
48-
if (now > cacheTimerLastSet + 1_000) {
49-
clearTimeout(cacheTimer)
50-
cacheTimer = setTimeout(function () {
43+
if (NODE_MAJOR < 20) return value
44+
cacheTime = Date.now()
45+
setCacheTTL()
46+
cache.set(key, value)
47+
return value
48+
}
49+
50+
function setCacheTTL () {
51+
if (cacheTimer !== null) return
52+
53+
cacheTimer = setTimeout(function () {
54+
cacheTimer = null
55+
if (Date.now() - cacheTime < 2_500) {
56+
// If the last cache entry was added recently, keep the cache alive
57+
setCacheTTL()
58+
} else {
5159
// Optimize for app boot, where a lot of reads might happen
5260
// Clear cache a few seconds after it was last used
5361
cache.clear()
54-
}, 10_000).unref()
55-
cacheTimerLastSet = now
56-
}
57-
cache.set(key, value)
58-
return value
62+
}
63+
}, 5_000).unref()
5964
}
6065

6166
function loadInlineSourceMap (data) {

packages/dd-trace/test/debugger/devtools_client/source-maps.spec.js

+61-7
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use strict'
22

33
require('../../setup/mocha')
4+
const { NODE_MAJOR } = require('../../../../../version')
45

56
const parsedSourceMap = {
67
version: 3,
@@ -94,13 +95,62 @@ describe('source map utils', function () {
9495
})
9596
})
9697

98+
describe('no cache', function () {
99+
function setup () {
100+
readFileSync = sinon.stub().returns(rawSourceMap)
101+
readFile = sinon.stub().resolves(rawSourceMap)
102+
103+
const sourceMaps = proxyquire('../src/debugger/devtools_client/source-maps', {
104+
fs: { readFileSync },
105+
'fs/promises': { readFile }
106+
})
107+
108+
loadSourceMap = sourceMaps.loadSourceMap
109+
loadSourceMapSync = sourceMaps.loadSourceMapSync
110+
}
111+
112+
before(function () {
113+
if (NODE_MAJOR > 18) this.skip()
114+
})
115+
116+
describe('loadSourceMap', function () {
117+
before(setup)
118+
119+
it('should read from disk on the fist call', async function () {
120+
const sourceMap = await loadSourceMap(dir, sourceMapURL)
121+
expect(sourceMap).to.deep.equal(parsedSourceMap)
122+
expect(readFile.callCount).to.equal(1)
123+
})
124+
125+
it('should read from disk on the second call', async function () {
126+
const sourceMap = await loadSourceMap(dir, sourceMapURL)
127+
expect(sourceMap).to.deep.equal(parsedSourceMap)
128+
expect(readFile.callCount).to.equal(2)
129+
})
130+
})
131+
132+
describe('loadSourceMapSync', function () {
133+
before(setup)
134+
135+
it('should read from disk on the fist call', function () {
136+
const sourceMap = loadSourceMapSync(dir, sourceMapURL)
137+
expect(sourceMap).to.deep.equal(parsedSourceMap)
138+
expect(readFileSync.callCount).to.equal(1)
139+
})
140+
141+
it('should read from disk on the second call', function () {
142+
const sourceMap = loadSourceMapSync(dir, sourceMapURL)
143+
expect(sourceMap).to.deep.equal(parsedSourceMap)
144+
expect(readFileSync.callCount).to.equal(2)
145+
})
146+
})
147+
})
148+
97149
describe('cache', function () {
98150
let clock
99151

100152
function setup () {
101-
clock = sinon.useFakeTimers({
102-
toFake: ['setTimeout']
103-
})
153+
clock = sinon.useFakeTimers()
104154
readFileSync = sinon.stub().returns(rawSourceMap)
105155
readFile = sinon.stub().resolves(rawSourceMap)
106156

@@ -117,6 +167,10 @@ describe('source map utils', function () {
117167
clock.restore()
118168
}
119169

170+
before(function () {
171+
if (NODE_MAJOR < 20) this.skip()
172+
})
173+
120174
describe('loadSourceMap', function () {
121175
before(setup)
122176

@@ -134,8 +188,8 @@ describe('source map utils', function () {
134188
expect(readFile.callCount).to.equal(1)
135189
})
136190

137-
it('should clear cache after 10 seconds', async function () {
138-
clock.tick(10_000)
191+
it('should clear cache after 5 seconds', async function () {
192+
clock.tick(5_000)
139193
const sourceMap = await loadSourceMap(dir, sourceMapURL)
140194
expect(sourceMap).to.deep.equal(parsedSourceMap)
141195
expect(readFile.callCount).to.equal(2)
@@ -159,8 +213,8 @@ describe('source map utils', function () {
159213
expect(readFileSync.callCount).to.equal(1)
160214
})
161215

162-
it('should clear cache after 10 seconds', function () {
163-
clock.tick(10_000)
216+
it('should clear cache after 5 seconds', function () {
217+
clock.tick(5_000)
164218
const sourceMap = loadSourceMapSync(dir, sourceMapURL)
165219
expect(sourceMap).to.deep.equal(parsedSourceMap)
166220
expect(readFileSync.callCount).to.equal(2)

0 commit comments

Comments
 (0)