diff --git a/packages/grpc-js/src/channel.ts b/packages/grpc-js/src/channel.ts index ebcf3a22..49f57e5c 100644 --- a/packages/grpc-js/src/channel.ts +++ b/packages/grpc-js/src/channel.ts @@ -288,6 +288,8 @@ export class ChannelImplementation implements Channel { close() { this.resolvingLoadBalancer.destroy(); this.updateState(ConnectivityState.SHUTDOWN); + + this.subchannelPool.unrefUnusedSubchannels(); } getTarget() { diff --git a/packages/grpc-js/src/subchannel-pool.ts b/packages/grpc-js/src/subchannel-pool.ts index 1a5015b6..1b8651cb 100644 --- a/packages/grpc-js/src/subchannel-pool.ts +++ b/packages/grpc-js/src/subchannel-pool.ts @@ -37,39 +37,71 @@ export class SubchannelPool { }; } = Object.create(null); + /** + * A timer of a task performing a periodic subchannel cleanup. + */ + private cleanupTimer: NodeJS.Timer | null = null; + /** * A pool of subchannels use for making connections. Subchannels with the * exact same parameters will be reused. * @param global If true, this is the global subchannel pool. Otherwise, it * is the pool for a single channel. */ - constructor(private global: boolean) { - if (global) { - setInterval(() => { - /* These objects are created with Object.create(null), so they do not - * have a prototype, which means that for (... in ...) loops over them - * do not need to be filtered */ - // tslint:disable-next-line:forin - for (const channelTarget in this.pool) { - // tslint:disable-next-line:forin - for (const subchannelTarget in this.pool[channelTarget]) { - const subchannelObjArray = this.pool[channelTarget][ - subchannelTarget - ]; - /* For each subchannel in the pool, try to unref it if it has - * exactly one ref (which is the ref from the pool itself). If that - * does happen, remove the subchannel from the pool */ - this.pool[channelTarget][ - subchannelTarget - ] = subchannelObjArray.filter( - value => !value.subchannel.unrefIfOneRef() - ); - } + constructor(private global: boolean) {} + + /** + * Unrefs all unused subchannels and cancels the cleanup task if all + * subchannels have been unrefed. + */ + unrefUnusedSubchannels(): void { + let allSubchannelsUnrefed = true; + + /* These objects are created with Object.create(null), so they do not + * have a prototype, which means that for (... in ...) loops over them + * do not need to be filtered */ + // tslint:disable-next-line:forin + for (const channelTarget in this.pool) { + // tslint:disable-next-line:forin + for (const subchannelTarget in this.pool[channelTarget]) { + const subchannelObjArray = this.pool[channelTarget][ + subchannelTarget + ]; + + const refedSubchannels = subchannelObjArray + .filter(value => !value.subchannel.unrefIfOneRef()); + + if (refedSubchannels.length > 0) { + allSubchannelsUnrefed = false; } - /* Currently we do not delete keys with empty values. If that results - * in significant memory usage we should change it. */ - }, REF_CHECK_INTERVAL).unref(); - // Unref because this timer should not keep the event loop running + + /* For each subchannel in the pool, try to unref it if it has + * exactly one ref (which is the ref from the pool itself). If that + * does happen, remove the subchannel from the pool */ + this.pool[channelTarget][subchannelTarget] = refedSubchannels; + } + } + /* Currently we do not delete keys with empty values. If that results + * in significant memory usage we should change it. */ + + // Cancel the cleanup task if all subchannels have been unrefed. + if (allSubchannelsUnrefed && this.cleanupTimer !== null) { + clearInterval(this.cleanupTimer); + this.cleanupTimer = null; + } + } + + /** + * Ensures that the cleanup task is spawned. + */ + ensureCleanupTask(): void { + if (this.global && this.cleanupTimer === null) { + this.cleanupTimer = setInterval(() => { + this.unrefUnusedSubchannels(); + }, REF_CHECK_INTERVAL); + + // Unref because this timer should not keep the event loop running. + this.cleanupTimer.unref(); } } @@ -87,6 +119,8 @@ export class SubchannelPool { channelArguments: ChannelOptions, channelCredentials: ChannelCredentials ): Subchannel { + this.ensureCleanupTask(); + if (channelTarget in this.pool) { if (subchannelTarget in this.pool[channelTarget]) { const subchannelObjArray = this.pool[channelTarget][subchannelTarget];