From bb6bb4a5ca79de4946c309f30cfffe0ebd893021 Mon Sep 17 00:00:00 2001 From: Michael Lumish <mlumish@google.com> Date: Tue, 7 Jan 2025 14:24:25 -0800 Subject: [PATCH 1/6] grpc-js-xds: Preserve resource type version and nonce when unsubscribing --- packages/grpc-js-xds/src/xds-client.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/grpc-js-xds/src/xds-client.ts b/packages/grpc-js-xds/src/xds-client.ts index 0879d6b27..eb37da978 100644 --- a/packages/grpc-js-xds/src/xds-client.ts +++ b/packages/grpc-js-xds/src/xds-client.ts @@ -457,9 +457,6 @@ class AdsCallState { if (authorityMap.size === 0) { typeState.subscribedResources.delete(name.authority); } - if (typeState.subscribedResources.size === 0) { - this.typeStates.delete(type); - } this.updateNames(type); } From b396b7d5ae2792fd34eefacf29d615b32cdfab5d Mon Sep 17 00:00:00 2001 From: Michael Lumish <mlumish@google.com> Date: Wed, 8 Jan 2025 14:56:38 -0800 Subject: [PATCH 2/6] grpc-js-xds: Bump to version 1.12.1 --- packages/grpc-js-xds/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grpc-js-xds/package.json b/packages/grpc-js-xds/package.json index f6cb72352..09fd3cdb5 100644 --- a/packages/grpc-js-xds/package.json +++ b/packages/grpc-js-xds/package.json @@ -1,6 +1,6 @@ { "name": "@grpc/grpc-js-xds", - "version": "1.12.0", + "version": "1.12.1", "description": "Plugin for @grpc/grpc-js. Adds the xds:// URL scheme and associated features.", "main": "build/src/index.js", "scripts": { From ca21e4ab1f3bc6938c4fbac7e00c428e18e11165 Mon Sep 17 00:00:00 2001 From: Michael Lumish <mlumish@google.com> Date: Fri, 31 Jan 2025 11:24:19 -0800 Subject: [PATCH 3/6] grpc-js: Allow garbage collection of IDLE channels --- packages/grpc-js/package.json | 2 +- packages/grpc-js/src/internal-channel.ts | 96 ++++++++++++++---------- 2 files changed, 59 insertions(+), 39 deletions(-) diff --git a/packages/grpc-js/package.json b/packages/grpc-js/package.json index b11495c6f..b54b1a8da 100644 --- a/packages/grpc-js/package.json +++ b/packages/grpc-js/package.json @@ -1,6 +1,6 @@ { "name": "@grpc/grpc-js", - "version": "1.12.5", + "version": "1.12.6", "description": "gRPC Library for Node - pure JS implementation", "homepage": "https://grpc.io/", "repository": "https://github.com/grpc/grpc-node/tree/master/packages/grpc-js", diff --git a/packages/grpc-js/src/internal-channel.ts b/packages/grpc-js/src/internal-channel.ts index 874729790..d53a9dc26 100644 --- a/packages/grpc-js/src/internal-channel.ts +++ b/packages/grpc-js/src/internal-channel.ts @@ -125,10 +125,13 @@ class ChannelSubchannelWrapper ) => { channel.throttleKeepalive(keepaliveTime); }; - childSubchannel.addConnectivityStateListener(this.subchannelStateListener); } ref(): void { + if (this.refCount === 0) { + this.child.addConnectivityStateListener(this.subchannelStateListener); + this.channel.addWrappedSubchannel(this); + } this.child.ref(); this.refCount += 1; } @@ -159,6 +162,26 @@ class ShutdownPicker implements Picker { } } +class ChannelzInfoTracker { + readonly trace = new ChannelzTrace(); + readonly callTracker = new ChannelzCallTracker(); + readonly childrenTracker = new ChannelzChildrenTracker(); + state: ConnectivityState = ConnectivityState.IDLE; + constructor(private target: string) {} + + getChannelzInfoCallback(): () => ChannelInfo { + return () => { + return { + target: this.target, + state: this.state, + trace: this.trace, + callTracker: this.callTracker, + children: this.childrenTracker.getChildLists() + }; + }; + } +} + export class InternalChannel { private readonly resolvingLoadBalancer: ResolvingLoadBalancer; private readonly subchannelPool: SubchannelPool; @@ -179,9 +202,10 @@ export class InternalChannel { * event loop open while there are any pending calls for the channel that * have not yet been assigned to specific subchannels. In other words, * the invariant is that callRefTimer is reffed if and only if pickQueue - * is non-empty. + * is non-empty. In addition, the timer is null while the state is IDLE or + * SHUTDOWN and there are no pending calls. */ - private readonly callRefTimer: NodeJS.Timeout; + private callRefTimer: NodeJS.Timeout | null = null; private configSelector: ConfigSelector | null = null; /** * This is the error from the name resolver if it failed most recently. It @@ -203,11 +227,8 @@ export class InternalChannel { // Channelz info private readonly channelzEnabled: boolean = true; - private readonly originalTarget: string; private readonly channelzRef: ChannelRef; - private readonly channelzTrace: ChannelzTrace; - private readonly callTracker = new ChannelzCallTracker(); - private readonly childrenTracker = new ChannelzChildrenTracker(); + private readonly channelzInfoTracker: ChannelzInfoTracker; /** * Randomly generated ID to be passed to the config selector, for use by @@ -236,7 +257,7 @@ export class InternalChannel { throw new TypeError('Channel options must be an object'); } } - this.originalTarget = target; + this.channelzInfoTracker = new ChannelzInfoTracker(target); const originalTargetUri = parseUri(target); if (originalTargetUri === null) { throw new Error(`Could not parse target name "${target}"`); @@ -250,21 +271,17 @@ export class InternalChannel { ); } - this.callRefTimer = setInterval(() => {}, MAX_TIMEOUT_TIME); - this.callRefTimer.unref?.(); - if (this.options['grpc.enable_channelz'] === 0) { this.channelzEnabled = false; } - this.channelzTrace = new ChannelzTrace(); this.channelzRef = registerChannelzChannel( target, - () => this.getChannelzInfo(), + this.channelzInfoTracker.getChannelzInfoCallback(), this.channelzEnabled ); if (this.channelzEnabled) { - this.channelzTrace.addTrace('CT_INFO', 'Channel created'); + this.channelzInfoTracker.trace.addTrace('CT_INFO', 'Channel created'); } if (this.options['grpc.default_authority']) { @@ -305,7 +322,7 @@ export class InternalChannel { ); subchannel.throttleKeepalive(this.keepaliveTime); if (this.channelzEnabled) { - this.channelzTrace.addTrace( + this.channelzInfoTracker.trace.addTrace( 'CT_INFO', 'Created subchannel or used existing subchannel', subchannel.getChannelzRef() @@ -315,7 +332,6 @@ export class InternalChannel { subchannel, this ); - this.wrappedSubchannels.add(wrappedSubchannel); return wrappedSubchannel; }, updateState: (connectivityState: ConnectivityState, picker: Picker) => { @@ -338,12 +354,12 @@ export class InternalChannel { }, addChannelzChild: (child: ChannelRef | SubchannelRef) => { if (this.channelzEnabled) { - this.childrenTracker.refChild(child); + this.channelzInfoTracker.childrenTracker.refChild(child); } }, removeChannelzChild: (child: ChannelRef | SubchannelRef) => { if (this.channelzEnabled) { - this.childrenTracker.unrefChild(child); + this.channelzInfoTracker.childrenTracker.unrefChild(child); } }, }; @@ -366,7 +382,7 @@ export class InternalChannel { RETRY_THROTTLER_MAP.delete(this.getTarget()); } if (this.channelzEnabled) { - this.channelzTrace.addTrace( + this.channelzInfoTracker.trace.addTrace( 'CT_INFO', 'Address resolution succeeded' ); @@ -388,7 +404,7 @@ export class InternalChannel { }, status => { if (this.channelzEnabled) { - this.channelzTrace.addTrace( + this.channelzInfoTracker.trace.addTrace( 'CT_WARNING', 'Address resolution failed with code ' + status.code + @@ -440,16 +456,6 @@ export class InternalChannel { this.lastActivityTimestamp = new Date(); } - private getChannelzInfo(): ChannelInfo { - return { - target: this.originalTarget, - state: this.connectivityState, - trace: this.channelzTrace, - callTracker: this.callTracker, - children: this.childrenTracker.getChildLists(), - }; - } - private trace(text: string, verbosityOverride?: LogVerbosity) { trace( verbosityOverride ?? LogVerbosity.DEBUG, @@ -459,6 +465,9 @@ export class InternalChannel { } private callRefTimerRef() { + if (!this.callRefTimer) { + this.callRefTimer = setInterval(() => {}, MAX_TIMEOUT_TIME) + } // If the hasRef function does not exist, always run the code if (!this.callRefTimer.hasRef?.()) { this.trace( @@ -472,15 +481,15 @@ export class InternalChannel { } private callRefTimerUnref() { - // If the hasRef function does not exist, always run the code - if (!this.callRefTimer.hasRef || this.callRefTimer.hasRef()) { + // If the timer or the hasRef function does not exist, always run the code + if (!this.callRefTimer?.hasRef || this.callRefTimer.hasRef()) { this.trace( 'callRefTimer.unref | configSelectionQueue.length=' + this.configSelectionQueue.length + ' pickQueue.length=' + this.pickQueue.length ); - this.callRefTimer.unref?.(); + this.callRefTimer?.unref?.(); } } @@ -509,12 +518,13 @@ export class InternalChannel { ConnectivityState[newState] ); if (this.channelzEnabled) { - this.channelzTrace.addTrace( + this.channelzInfoTracker.trace.addTrace( 'CT_INFO', 'Connectivity state change to ' + ConnectivityState[newState] ); } this.connectivityState = newState; + this.channelzInfoTracker.state = newState; const watchersCopy = this.connectivityStateWatchers.slice(); for (const watcherObject of watchersCopy) { if (newState !== watcherObject.currentState) { @@ -539,6 +549,10 @@ export class InternalChannel { } } + addWrappedSubchannel(wrappedSubchannel: ChannelSubchannelWrapper) { + this.wrappedSubchannels.add(wrappedSubchannel); + } + removeWrappedSubchannel(wrappedSubchannel: ChannelSubchannelWrapper) { this.wrappedSubchannels.delete(wrappedSubchannel); } @@ -591,6 +605,10 @@ export class InternalChannel { clearTimeout(this.idleTimer); this.idleTimer = null; } + if (this.callRefTimer) { + clearInterval(this.callRefTimer); + this.callRefTimer = null; + } } private startIdleTimeout(timeoutMs: number) { @@ -634,7 +652,7 @@ export class InternalChannel { private onCallStart() { if (this.channelzEnabled) { - this.callTracker.addCallStarted(); + this.channelzInfoTracker.callTracker.addCallStarted(); } this.callCount += 1; } @@ -642,9 +660,9 @@ export class InternalChannel { private onCallEnd(status: StatusObject) { if (this.channelzEnabled) { if (status.code === Status.OK) { - this.callTracker.addCallSucceeded(); + this.channelzInfoTracker.callTracker.addCallSucceeded(); } else { - this.callTracker.addCallFailed(); + this.channelzInfoTracker.callTracker.addCallFailed(); } } this.callCount -= 1; @@ -776,7 +794,9 @@ export class InternalChannel { call.cancelWithStatus(Status.UNAVAILABLE, 'Channel closed before call started'); } this.pickQueue = []; - clearInterval(this.callRefTimer); + if (this.callRefTimer) { + clearInterval(this.callRefTimer); + } if (this.idleTimer) { clearTimeout(this.idleTimer); } From 621f401e348776c411b101e768ced63ed017055e Mon Sep 17 00:00:00 2001 From: gfrancz <52467377+gfrancz@users.noreply.github.com> Date: Mon, 27 Jan 2025 21:46:34 -0500 Subject: [PATCH 4/6] fix: xds-client LrsCallState statsTimer memory leak --- packages/grpc-js-xds/src/xds-client.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/grpc-js-xds/src/xds-client.ts b/packages/grpc-js-xds/src/xds-client.ts index eb37da978..71b03ab5e 100644 --- a/packages/grpc-js-xds/src/xds-client.ts +++ b/packages/grpc-js-xds/src/xds-client.ts @@ -682,6 +682,13 @@ class LrsCallState { this.sendStats(); } + destroy() { + if (this.statsTimer) { + this.statsTimer = clearInterval(this.statsTimer); + } + return null; + } + private handleStreamStatus(status: StatusObject) { this.client.trace( 'LRS stream ended. code=' + status.code + ' details= ' + status.details @@ -932,7 +939,7 @@ class XdsSingleServerClient { } handleLrsStreamEnd() { - this.lrsCallState = null; + this.lrsCallState = this.lrsCallState ? this.lrsCallState.destroy() : null; /* The backoff timer would start the stream when it finishes. If it is not * running, restart the stream immediately. */ if (!this.lrsBackoff.isRunning()) { From 588b69c12c884991c415665d2e37492464ba6eb8 Mon Sep 17 00:00:00 2001 From: gfrancz <52467377+gfrancz@users.noreply.github.com> Date: Tue, 4 Feb 2025 13:55:16 -0500 Subject: [PATCH 5/6] Fix type issue by setting the attribute separately Co-authored-by: ws-gregm <55088361+ws-gregm@users.noreply.github.com> --- packages/grpc-js-xds/src/xds-client.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/grpc-js-xds/src/xds-client.ts b/packages/grpc-js-xds/src/xds-client.ts index 71b03ab5e..b4d6e24fc 100644 --- a/packages/grpc-js-xds/src/xds-client.ts +++ b/packages/grpc-js-xds/src/xds-client.ts @@ -684,7 +684,8 @@ class LrsCallState { destroy() { if (this.statsTimer) { - this.statsTimer = clearInterval(this.statsTimer); + clearInterval(this.statsTimer); + this.statsTimer = null; } return null; } From f1f2b2dd83cf0c3e92e391db85e05b3a9761436c Mon Sep 17 00:00:00 2001 From: Michael Lumish <mlumish@google.com> Date: Tue, 4 Feb 2025 14:14:37 -0800 Subject: [PATCH 6/6] grpc-js-xds: Bump to version 1.12.2 --- packages/grpc-js-xds/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grpc-js-xds/package.json b/packages/grpc-js-xds/package.json index 09fd3cdb5..512ad77ed 100644 --- a/packages/grpc-js-xds/package.json +++ b/packages/grpc-js-xds/package.json @@ -1,6 +1,6 @@ { "name": "@grpc/grpc-js-xds", - "version": "1.12.1", + "version": "1.12.2", "description": "Plugin for @grpc/grpc-js. Adds the xds:// URL scheme and associated features.", "main": "build/src/index.js", "scripts": {