Skip to content

Commit faca33a

Browse files
Dolan Halbrookmetcoder95
andauthored
Add "clientTtl" option to close and remove connections from the pool after a specified time. (#4175)
Co-authored-by: Carlos Fuentes <me@metcoder.dev>
1 parent 1333929 commit faca33a

4 files changed

Lines changed: 70 additions & 3 deletions

File tree

docs/docs/api/Pool.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ Extends: [`ClientOptions`](/docs/docs/api/Client.md#parameter-clientoptions)
1919

2020
* **factory** `(origin: URL, opts: Object) => Dispatcher` - Default: `(origin, opts) => new Client(origin, opts)`
2121
* **connections** `number | null` (optional) - Default: `null` - The number of `Client` instances to create. When set to `null`, the `Pool` instance will create an unlimited amount of `Client` instances.
22+
* **clientTtl** `number | null` (optional) - Default: `null` - The amount of time before a `Client` instance is removed from the `Pool` and closed. When set to `null`, `Client` instances will not be removed or closed based on age.
2223

2324
## Instance Properties
2425

lib/dispatcher/pool.js

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ const {
55
kClients,
66
kNeedDrain,
77
kAddClient,
8-
kGetDispatcher
8+
kGetDispatcher,
9+
kRemoveClient
910
} = require('./pool-base')
1011
const Client = require('./client')
1112
const {
@@ -35,6 +36,7 @@ class Pool extends PoolBase {
3536
autoSelectFamily,
3637
autoSelectFamilyAttemptTimeout,
3738
allowH2,
39+
clientTtl,
3840
...options
3941
} = {}) {
4042
if (connections != null && (!Number.isFinite(connections) || connections < 0)) {
@@ -65,12 +67,20 @@ class Pool extends PoolBase {
6567

6668
this[kConnections] = connections || null
6769
this[kUrl] = util.parseOrigin(origin)
68-
this[kOptions] = { ...util.deepClone(options), connect, allowH2 }
70+
this[kOptions] = { ...util.deepClone(options), connect, allowH2, clientTtl }
6971
this[kOptions].interceptors = options.interceptors
7072
? { ...options.interceptors }
7173
: undefined
7274
this[kFactory] = factory
7375

76+
this.on('connect', (origin, targets) => {
77+
if (clientTtl != null && clientTtl > 0) {
78+
for (const target of targets) {
79+
Object.assign(target, { ttl: Date.now() })
80+
}
81+
}
82+
})
83+
7484
this.on('connectionError', (origin, targets, error) => {
7585
// If a connection error occurs, we remove the client from the pool,
7686
// and emit a connectionError event. They will not be re-used.
@@ -87,8 +97,12 @@ class Pool extends PoolBase {
8797
}
8898

8999
[kGetDispatcher] () {
100+
const clientTtlOption = this[kOptions].clientTtl
90101
for (const client of this[kClients]) {
91-
if (!client[kNeedDrain]) {
102+
// check ttl of client and if it's stale, remove it from the pool
103+
if (clientTtlOption != null && clientTtlOption > 0 && client.ttl && ((Date.now() - client.ttl) > clientTtlOption)) {
104+
this[kRemoveClient](client)
105+
} else if (!client[kNeedDrain]) {
92106
return client
93107
}
94108
}

test/pool.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -578,6 +578,56 @@ test('pool connect', async (t) => {
578578
await t.completed
579579
})
580580

581+
test('pool connect with clientTtl specified', async (t) => {
582+
t = tspl(t, { plan: 1 })
583+
584+
const server = createServer({ joinDuplicateHeaders: true }, (c) => {
585+
t.fail()
586+
})
587+
server.on('connect', (req, socket, firstBodyChunk) => {
588+
socket.write('HTTP/1.1 200 Connection established\r\n\r\n')
589+
590+
let data = firstBodyChunk.toString()
591+
socket.on('data', (buf) => {
592+
data += buf.toString()
593+
})
594+
595+
socket.on('end', () => {
596+
socket.end(data)
597+
})
598+
})
599+
after(() => server.close())
600+
601+
server.listen(0, async () => {
602+
const client = new Pool(`http://localhost:${server.address().port}`, {
603+
clientTtl: 10
604+
})
605+
606+
const { socket } = await client.connect({
607+
path: '/'
608+
})
609+
610+
t.strictEqual(socket.closed, false)
611+
612+
let recvData = ''
613+
socket.on('data', (d) => {
614+
recvData += d
615+
})
616+
617+
socket.on('end', () => {
618+
t.strictEqual(recvData.toString(), 'Body')
619+
})
620+
621+
socket.write('Body')
622+
socket.end()
623+
624+
await new Promise(resolve => setTimeout(resolve, 10))
625+
t.strictEqual(socket.closed, true)
626+
})
627+
628+
await t.completed
629+
})
630+
581631
test('pool dispatch', async (t) => {
582632
t = tspl(t, { plan: 2 })
583633

types/pool.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ declare namespace Pool {
3333
factory?(origin: URL, opts: object): Dispatcher;
3434
/** The max number of clients to create. `null` if no limit. Default `null`. */
3535
connections?: number | null;
36+
/** The amount of time before a client is removed from the pool and closed. `null` if no time limit. Default `null` */
37+
clientTtl?: number | null;
3638

3739
interceptors?: { Pool?: readonly Dispatcher.DispatchInterceptor[] } & Client.Options['interceptors']
3840
}

0 commit comments

Comments
 (0)