From 4e135a099c4e944954f68d894a165580a997e0f8 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Wed, 9 Sep 2026 14:58:54 -0700 Subject: [PATCH 1/2] grpc-js: Remove calls from the pick queue immediately when they end --- packages/grpc-js/src/internal-channel.ts | 27 +++++++++++++-------- packages/grpc-js/src/load-balancing-call.ts | 1 + 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/packages/grpc-js/src/internal-channel.ts b/packages/grpc-js/src/internal-channel.ts index db3827f7b..cc3f5a0af 100644 --- a/packages/grpc-js/src/internal-channel.ts +++ b/packages/grpc-js/src/internal-channel.ts @@ -193,7 +193,7 @@ export class InternalChannel { * first time the resolver returns a result, which includes the ConfigSelector. */ private configSelectionQueue: ResolvingCall[] = []; - private pickQueue: LoadBalancingCall[] = []; + private pickQueue: Set = new Set(); private connectivityStateWatchers: ConnectivityStateWatcher[] = []; private readonly defaultAuthority: string; private readonly filterStackFactory: FilterStackFactory; @@ -342,9 +342,9 @@ export class InternalChannel { }, updateState: (connectivityState: ConnectivityState, picker: Picker) => { this.currentPicker = picker; - const queueCopy = this.pickQueue.slice(); - this.pickQueue = []; - if (queueCopy.length > 0) { + const queueCopy = {...this.pickQueue}; + this.pickQueue = new Set(); + if (queueCopy.size > 0) { this.callRefTimerUnref(); } for (const call of queueCopy) { @@ -479,8 +479,8 @@ export class InternalChannel { this.trace( 'callRefTimer.ref | configSelectionQueue.length=' + this.configSelectionQueue.length + - ' pickQueue.length=' + - this.pickQueue.length + ' pickQueue.size=' + + this.pickQueue.size ); this.callRefTimer.ref?.(); } @@ -492,8 +492,8 @@ export class InternalChannel { this.trace( 'callRefTimer.unref | configSelectionQueue.length=' + this.configSelectionQueue.length + - ' pickQueue.length=' + - this.pickQueue.length + ' pickQueue.size=' + + this.pickQueue.size ); this.callRefTimer?.unref?.(); } @@ -571,10 +571,17 @@ export class InternalChannel { } queueCallForPick(call: LoadBalancingCall) { - this.pickQueue.push(call); + this.pickQueue.add(call); this.callRefTimerRef(); } + removeCallFromPickQueue(call: LoadBalancingCall) { + this.pickQueue.delete(call); + if (this.pickQueue.size === 0) { + this.callRefTimerUnref(); + } + } + getConfig(method: string, metadata: Metadata): GetConfigResult { if (this.connectivityState !== ConnectivityState.SHUTDOWN) { this.resolvingLoadBalancer.exitIdle(); @@ -771,7 +778,7 @@ export class InternalChannel { for (const call of this.pickQueue) { call.cancelWithStatus(Status.UNAVAILABLE, 'Channel closed before call started'); } - this.pickQueue = []; + this.pickQueue.clear(); if (this.callRefTimer) { clearInterval(this.callRefTimer); } diff --git a/packages/grpc-js/src/load-balancing-call.ts b/packages/grpc-js/src/load-balancing-call.ts index 3ff728985..36014ee4f 100644 --- a/packages/grpc-js/src/load-balancing-call.ts +++ b/packages/grpc-js/src/load-balancing-call.ts @@ -128,6 +128,7 @@ export class LoadBalancingCall implements Call, DeadlineInfoProvider { const finalStatus = { ...status, progress }; this.listener?.onReceiveStatus(finalStatus); this.onCallEnded?.(finalStatus.code, finalStatus.details, finalStatus.metadata); + this.channel.removeCallFromPickQueue(this); } } From 9bf01da0229f42154f5337488cbb66ffc89915c7 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Thu, 10 Sep 2026 09:45:10 -0700 Subject: [PATCH 2/2] Don't copy the pick queue to loop over it Co-authored-by: Sergey Melikyan <55602957+Sermelyan@users.noreply.github.com> --- packages/grpc-js/src/internal-channel.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grpc-js/src/internal-channel.ts b/packages/grpc-js/src/internal-channel.ts index cc3f5a0af..942d7a449 100644 --- a/packages/grpc-js/src/internal-channel.ts +++ b/packages/grpc-js/src/internal-channel.ts @@ -342,7 +342,7 @@ export class InternalChannel { }, updateState: (connectivityState: ConnectivityState, picker: Picker) => { this.currentPicker = picker; - const queueCopy = {...this.pickQueue}; + const queueCopy = this.pickQueue; this.pickQueue = new Set(); if (queueCopy.size > 0) { this.callRefTimerUnref();