Skip to content

Commit ab6ea16

Browse files
authored
fix: memory leak in Agent (#4425)
1 parent 74d505f commit ab6ea16

2 files changed

Lines changed: 87 additions & 16 deletions

File tree

lib/dispatcher/agent.js

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -45,27 +45,14 @@ class Agent extends DispatcherBase {
4545
}
4646

4747
this[kOnConnect] = (origin, targets) => {
48-
const result = this[kClients].get(origin)
49-
if (result) {
50-
result.count += 1
51-
}
5248
this.emit('connect', origin, [this, ...targets])
5349
}
5450

5551
this[kOnDisconnect] = (origin, targets, err) => {
56-
const result = this[kClients].get(origin)
57-
if (result) {
58-
result.count -= 1
59-
if (result.count <= 0) {
60-
this[kClients].delete(origin)
61-
result.dispatcher.destroy()
62-
}
63-
}
6452
this.emit('disconnect', origin, [this, ...targets], err)
6553
}
6654

6755
this[kOnConnectionError] = (origin, targets, err) => {
68-
// TODO: should this decrement result.count here?
6956
this.emit('connectionError', origin, [this, ...targets], err)
7057
}
7158
}
@@ -89,11 +76,33 @@ class Agent extends DispatcherBase {
8976
const result = this[kClients].get(key)
9077
let dispatcher = result && result.dispatcher
9178
if (!dispatcher) {
79+
const closeClientIfUnused = (connected) => {
80+
const result = this[kClients].get(key)
81+
if (result) {
82+
if (connected) result.count -= 1
83+
if (result.count <= 0) {
84+
this[kClients].delete(key)
85+
result.dispatcher.close()
86+
}
87+
}
88+
}
9289
dispatcher = this[kFactory](opts.origin, this[kOptions])
9390
.on('drain', this[kOnDrain])
94-
.on('connect', this[kOnConnect])
95-
.on('disconnect', this[kOnDisconnect])
96-
.on('connectionError', this[kOnConnectionError])
91+
.on('connect', (origin, targets) => {
92+
const result = this[kClients].get(key)
93+
if (result) {
94+
result.count += 1
95+
}
96+
this[kOnConnect](origin, targets)
97+
})
98+
.on('disconnect', (origin, targets, err) => {
99+
closeClientIfUnused(true)
100+
this[kOnDisconnect](origin, targets, err)
101+
})
102+
.on('connectionError', (origin, targets, err) => {
103+
closeClientIfUnused(false)
104+
this[kOnConnectionError](origin, targets, err)
105+
})
97106

98107
this[kClients].set(key, { count: 0, dispatcher })
99108
}

test/issue-4244.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
const { test, describe } = require('node:test')
2+
const assert = require('node:assert')
3+
const { createServer } = require('node:http')
4+
const { request, Agent, Pool } = require('..')
5+
6+
// https://github.com/nodejs/undici/issues/4424
7+
describe('Agent should close inactive clients', () => {
8+
test('without active connections', async (t) => {
9+
const server = createServer({ keepAliveTimeout: 0 }, async (_req, res) => {
10+
res.setHeader('connection', 'close')
11+
res.writeHead(200)
12+
res.end('ok')
13+
}).listen(0)
14+
15+
t.after(() => server.close())
16+
17+
let p
18+
const agent = new Agent({
19+
factory: (origin, opts) => {
20+
const pool = new Pool(origin, opts)
21+
let _resolve, _reject
22+
p = new Promise((resolve, reject) => {
23+
_resolve = resolve
24+
_reject = reject
25+
})
26+
pool.on('disconnect', () => {
27+
setImmediate(() => pool.destroyed ? _resolve() : _reject(new Error('client not destroyed')))
28+
})
29+
return pool
30+
}
31+
})
32+
const { statusCode } = await request(`http://localhost:${server.address().port}`, { dispatcher: agent })
33+
assert.equal(statusCode, 200)
34+
35+
await p
36+
})
37+
38+
test('in case of connection error', async (t) => {
39+
let p
40+
const agent = new Agent({
41+
factory: (origin, opts) => {
42+
const pool = new Pool(origin, opts)
43+
let _resolve, _reject
44+
p = new Promise((resolve, reject) => {
45+
_resolve = resolve
46+
_reject = reject
47+
})
48+
pool.on('connectionError', () => {
49+
setImmediate(() => pool.destroyed ? _resolve() : _reject(new Error('client not destroyed')))
50+
})
51+
return pool
52+
}
53+
})
54+
try {
55+
await request('http://localhost:0', { dispatcher: agent })
56+
} catch (_) {
57+
// ignore
58+
}
59+
60+
await p
61+
})
62+
})

0 commit comments

Comments
 (0)