Skip to content

Commit

Permalink
Remove FinalizationRegistry from Agent (#2530)
Browse files Browse the repository at this point in the history
* Remove FinalizationRegistry from Agent

Signed-off-by: Matteo Collina <hello@matteocollina.com>

* fixup

Signed-off-by: Matteo Collina <hello@matteocollina.com>

---------

Signed-off-by: Matteo Collina <hello@matteocollina.com>
  • Loading branch information
mcollina committed Dec 20, 2023
1 parent 250b89a commit 59c2f38
Show file tree
Hide file tree
Showing 2 changed files with 41 additions and 51 deletions.
69 changes: 35 additions & 34 deletions lib/agent.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,21 @@
'use strict'

const { InvalidArgumentError } = require('./core/errors')
const { kClients, kRunning, kClose, kDestroy, kDispatch, kInterceptors } = require('./core/symbols')
const { kClients, kRunning, kClose, kDestroy, kDispatch, kInterceptors, kBusy } = require('./core/symbols')
const DispatcherBase = require('./dispatcher-base')
const Pool = require('./pool')
const Client = require('./client')
const util = require('./core/util')
const createRedirectInterceptor = require('./interceptor/redirectInterceptor')
const { WeakRef, FinalizationRegistry } = require('./compat/dispatcher-weakref')()

const kOnConnect = Symbol('onConnect')
const kOnDisconnect = Symbol('onDisconnect')
const kOnConnectionError = Symbol('onConnectionError')
const kMaxRedirections = Symbol('maxRedirections')
const kOnDrain = Symbol('onDrain')
const kFactory = Symbol('factory')
const kFinalizer = Symbol('finalizer')
const kOptions = Symbol('options')
const kDeleteScheduled = Symbol('deleteScheduled')

function defaultFactory (origin, opts) {
return opts && opts.connections === 1
Expand Down Expand Up @@ -55,12 +54,6 @@ class Agent extends DispatcherBase {
this[kMaxRedirections] = maxRedirections
this[kFactory] = factory
this[kClients] = new Map()
this[kFinalizer] = new FinalizationRegistry(/* istanbul ignore next: gc is undeterministic */ key => {
const ref = this[kClients].get(key)
if (ref !== undefined && ref.deref() === undefined) {
this[kClients].delete(key)
}
})

const agent = this

Expand All @@ -83,12 +76,8 @@ class Agent extends DispatcherBase {

get [kRunning] () {
let ret = 0
for (const ref of this[kClients].values()) {
const client = ref.deref()
/* istanbul ignore next: gc is undeterministic */
if (client) {
ret += client[kRunning]
}
for (const client of this[kClients].values()) {
ret += client[kRunning]
}
return ret
}
Expand All @@ -101,47 +90,59 @@ class Agent extends DispatcherBase {
throw new InvalidArgumentError('opts.origin must be a non-empty string or URL.')
}

const ref = this[kClients].get(key)
let dispatcher = this[kClients].get(key)

let dispatcher = ref ? ref.deref() : null
if (!dispatcher) {
dispatcher = this[kFactory](opts.origin, this[kOptions])
.on('drain', this[kOnDrain])
.on('drain', (...args) => {
this[kOnDrain](...args)

// We remove the client if it is not busy for 5 minutes
// to avoid a long list of clients to saturate memory.
// Ideally, we could use a FinalizationRegistry here, but
// it is currently very buggy in Node.js.
// See
// * https://github.com/nodejs/node/issues/49344
// * https://github.com/nodejs/node/issues/47748
// TODO(mcollina): make the timeout configurable or
// use an event to remove disconnected clients.
this[kDeleteScheduled] = setTimeout(() => {
if (dispatcher[kBusy] === 0) {
this[kClients].destroy().then(() => {})
this[kClients].delete(key)
}
}, 300_000)
this[kDeleteScheduled].unref()
})
.on('connect', this[kOnConnect])
.on('disconnect', this[kOnDisconnect])
.on('connectionError', this[kOnConnectionError])

this[kClients].set(key, new WeakRef(dispatcher))
this[kFinalizer].register(dispatcher, key)
this[kClients].set(key, dispatcher)
} else if (dispatcher[kDeleteScheduled]) {
clearTimeout(dispatcher[kDeleteScheduled])
dispatcher[kDeleteScheduled] = null
}

return dispatcher.dispatch(opts, handler)
}

async [kClose] () {
const closePromises = []
for (const ref of this[kClients].values()) {
const client = ref.deref()
/* istanbul ignore else: gc is undeterministic */
if (client) {
this[kFinalizer].unregister(client)
closePromises.push(client.close())
}
for (const client of this[kClients].values()) {
closePromises.push(client.close())
}
this[kClients].clear()

await Promise.all(closePromises)
}

async [kDestroy] (err) {
const destroyPromises = []
for (const ref of this[kClients].values()) {
const client = ref.deref()
/* istanbul ignore else: gc is undeterministic */
if (client) {
this[kFinalizer].unregister(client)
destroyPromises.push(client.destroy(err))
}
for (const client of this[kClients].values()) {
destroyPromises.push(client.destroy(err))
}
this[kClients].clear()

await Promise.all(destroyPromises)
}
Expand Down
23 changes: 6 additions & 17 deletions lib/mock/mock-agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,6 @@ const Dispatcher = require('../dispatcher')
const Pluralizer = require('./pluralizer')
const PendingInterceptorsFormatter = require('./pending-interceptors-formatter')

class FakeWeakRef {
constructor (value) {
this.value = value
}

deref () {
return this.value
}
}

class MockAgent extends Dispatcher {
constructor (opts) {
super(opts)
Expand Down Expand Up @@ -103,7 +93,7 @@ class MockAgent extends Dispatcher {
}

[kMockAgentSet] (origin, dispatcher) {
this[kClients].set(origin, new FakeWeakRef(dispatcher))
this[kClients].set(origin, dispatcher)
}

[kFactory] (origin) {
Expand All @@ -115,9 +105,9 @@ class MockAgent extends Dispatcher {

[kMockAgentGet] (origin) {
// First check if we can immediately find it
const ref = this[kClients].get(origin)
if (ref) {
return ref.deref()
const client = this[kClients].get(origin)
if (client) {
return client
}

// If the origin is not a string create a dummy parent pool and return to user
Expand All @@ -128,8 +118,7 @@ class MockAgent extends Dispatcher {
}

// If we match, create a pool and assign the same dispatches
for (const [keyMatcher, nonExplicitRef] of Array.from(this[kClients])) {
const nonExplicitDispatcher = nonExplicitRef.deref()
for (const [keyMatcher, nonExplicitDispatcher] of Array.from(this[kClients])) {
if (nonExplicitDispatcher && typeof keyMatcher !== 'string' && matchValue(keyMatcher, origin)) {
const dispatcher = this[kFactory](origin)
this[kMockAgentSet](origin, dispatcher)
Expand All @@ -147,7 +136,7 @@ class MockAgent extends Dispatcher {
const mockAgentClients = this[kClients]

return Array.from(mockAgentClients.entries())
.flatMap(([origin, scope]) => scope.deref()[kDispatches].map(dispatch => ({ ...dispatch, origin })))
.flatMap(([origin, scope]) => scope[kDispatches].map(dispatch => ({ ...dispatch, origin })))
.filter(({ pending }) => pending)
}

Expand Down

0 comments on commit 59c2f38

Please sign in to comment.