Skip to content

Commit 6bd791d

Browse files
authored
Merge pull request #2896 from murgatroid99/grpc-js_reduce_channel_leak
grpc-js: Allow garbage collection of IDLE channels
2 parents c6c69df + ca21e4a commit 6bd791d

File tree

2 files changed

+59
-39
lines changed

2 files changed

+59
-39
lines changed

packages/grpc-js/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@grpc/grpc-js",
3-
"version": "1.12.5",
3+
"version": "1.12.6",
44
"description": "gRPC Library for Node - pure JS implementation",
55
"homepage": "https://grpc.io/",
66
"repository": "https://github.com/grpc/grpc-node/tree/master/packages/grpc-js",

packages/grpc-js/src/internal-channel.ts

+58-38
Original file line numberDiff line numberDiff line change
@@ -125,10 +125,13 @@ class ChannelSubchannelWrapper
125125
) => {
126126
channel.throttleKeepalive(keepaliveTime);
127127
};
128-
childSubchannel.addConnectivityStateListener(this.subchannelStateListener);
129128
}
130129

131130
ref(): void {
131+
if (this.refCount === 0) {
132+
this.child.addConnectivityStateListener(this.subchannelStateListener);
133+
this.channel.addWrappedSubchannel(this);
134+
}
132135
this.child.ref();
133136
this.refCount += 1;
134137
}
@@ -159,6 +162,26 @@ class ShutdownPicker implements Picker {
159162
}
160163
}
161164

165+
class ChannelzInfoTracker {
166+
readonly trace = new ChannelzTrace();
167+
readonly callTracker = new ChannelzCallTracker();
168+
readonly childrenTracker = new ChannelzChildrenTracker();
169+
state: ConnectivityState = ConnectivityState.IDLE;
170+
constructor(private target: string) {}
171+
172+
getChannelzInfoCallback(): () => ChannelInfo {
173+
return () => {
174+
return {
175+
target: this.target,
176+
state: this.state,
177+
trace: this.trace,
178+
callTracker: this.callTracker,
179+
children: this.childrenTracker.getChildLists()
180+
};
181+
};
182+
}
183+
}
184+
162185
export class InternalChannel {
163186
private readonly resolvingLoadBalancer: ResolvingLoadBalancer;
164187
private readonly subchannelPool: SubchannelPool;
@@ -179,9 +202,10 @@ export class InternalChannel {
179202
* event loop open while there are any pending calls for the channel that
180203
* have not yet been assigned to specific subchannels. In other words,
181204
* the invariant is that callRefTimer is reffed if and only if pickQueue
182-
* is non-empty.
205+
* is non-empty. In addition, the timer is null while the state is IDLE or
206+
* SHUTDOWN and there are no pending calls.
183207
*/
184-
private readonly callRefTimer: NodeJS.Timeout;
208+
private callRefTimer: NodeJS.Timeout | null = null;
185209
private configSelector: ConfigSelector | null = null;
186210
/**
187211
* This is the error from the name resolver if it failed most recently. It
@@ -203,11 +227,8 @@ export class InternalChannel {
203227

204228
// Channelz info
205229
private readonly channelzEnabled: boolean = true;
206-
private readonly originalTarget: string;
207230
private readonly channelzRef: ChannelRef;
208-
private readonly channelzTrace: ChannelzTrace;
209-
private readonly callTracker = new ChannelzCallTracker();
210-
private readonly childrenTracker = new ChannelzChildrenTracker();
231+
private readonly channelzInfoTracker: ChannelzInfoTracker;
211232

212233
/**
213234
* Randomly generated ID to be passed to the config selector, for use by
@@ -236,7 +257,7 @@ export class InternalChannel {
236257
throw new TypeError('Channel options must be an object');
237258
}
238259
}
239-
this.originalTarget = target;
260+
this.channelzInfoTracker = new ChannelzInfoTracker(target);
240261
const originalTargetUri = parseUri(target);
241262
if (originalTargetUri === null) {
242263
throw new Error(`Could not parse target name "${target}"`);
@@ -250,21 +271,17 @@ export class InternalChannel {
250271
);
251272
}
252273

253-
this.callRefTimer = setInterval(() => {}, MAX_TIMEOUT_TIME);
254-
this.callRefTimer.unref?.();
255-
256274
if (this.options['grpc.enable_channelz'] === 0) {
257275
this.channelzEnabled = false;
258276
}
259277

260-
this.channelzTrace = new ChannelzTrace();
261278
this.channelzRef = registerChannelzChannel(
262279
target,
263-
() => this.getChannelzInfo(),
280+
this.channelzInfoTracker.getChannelzInfoCallback(),
264281
this.channelzEnabled
265282
);
266283
if (this.channelzEnabled) {
267-
this.channelzTrace.addTrace('CT_INFO', 'Channel created');
284+
this.channelzInfoTracker.trace.addTrace('CT_INFO', 'Channel created');
268285
}
269286

270287
if (this.options['grpc.default_authority']) {
@@ -305,7 +322,7 @@ export class InternalChannel {
305322
);
306323
subchannel.throttleKeepalive(this.keepaliveTime);
307324
if (this.channelzEnabled) {
308-
this.channelzTrace.addTrace(
325+
this.channelzInfoTracker.trace.addTrace(
309326
'CT_INFO',
310327
'Created subchannel or used existing subchannel',
311328
subchannel.getChannelzRef()
@@ -315,7 +332,6 @@ export class InternalChannel {
315332
subchannel,
316333
this
317334
);
318-
this.wrappedSubchannels.add(wrappedSubchannel);
319335
return wrappedSubchannel;
320336
},
321337
updateState: (connectivityState: ConnectivityState, picker: Picker) => {
@@ -338,12 +354,12 @@ export class InternalChannel {
338354
},
339355
addChannelzChild: (child: ChannelRef | SubchannelRef) => {
340356
if (this.channelzEnabled) {
341-
this.childrenTracker.refChild(child);
357+
this.channelzInfoTracker.childrenTracker.refChild(child);
342358
}
343359
},
344360
removeChannelzChild: (child: ChannelRef | SubchannelRef) => {
345361
if (this.channelzEnabled) {
346-
this.childrenTracker.unrefChild(child);
362+
this.channelzInfoTracker.childrenTracker.unrefChild(child);
347363
}
348364
},
349365
};
@@ -366,7 +382,7 @@ export class InternalChannel {
366382
RETRY_THROTTLER_MAP.delete(this.getTarget());
367383
}
368384
if (this.channelzEnabled) {
369-
this.channelzTrace.addTrace(
385+
this.channelzInfoTracker.trace.addTrace(
370386
'CT_INFO',
371387
'Address resolution succeeded'
372388
);
@@ -388,7 +404,7 @@ export class InternalChannel {
388404
},
389405
status => {
390406
if (this.channelzEnabled) {
391-
this.channelzTrace.addTrace(
407+
this.channelzInfoTracker.trace.addTrace(
392408
'CT_WARNING',
393409
'Address resolution failed with code ' +
394410
status.code +
@@ -440,16 +456,6 @@ export class InternalChannel {
440456
this.lastActivityTimestamp = new Date();
441457
}
442458

443-
private getChannelzInfo(): ChannelInfo {
444-
return {
445-
target: this.originalTarget,
446-
state: this.connectivityState,
447-
trace: this.channelzTrace,
448-
callTracker: this.callTracker,
449-
children: this.childrenTracker.getChildLists(),
450-
};
451-
}
452-
453459
private trace(text: string, verbosityOverride?: LogVerbosity) {
454460
trace(
455461
verbosityOverride ?? LogVerbosity.DEBUG,
@@ -459,6 +465,9 @@ export class InternalChannel {
459465
}
460466

461467
private callRefTimerRef() {
468+
if (!this.callRefTimer) {
469+
this.callRefTimer = setInterval(() => {}, MAX_TIMEOUT_TIME)
470+
}
462471
// If the hasRef function does not exist, always run the code
463472
if (!this.callRefTimer.hasRef?.()) {
464473
this.trace(
@@ -472,15 +481,15 @@ export class InternalChannel {
472481
}
473482

474483
private callRefTimerUnref() {
475-
// If the hasRef function does not exist, always run the code
476-
if (!this.callRefTimer.hasRef || this.callRefTimer.hasRef()) {
484+
// If the timer or the hasRef function does not exist, always run the code
485+
if (!this.callRefTimer?.hasRef || this.callRefTimer.hasRef()) {
477486
this.trace(
478487
'callRefTimer.unref | configSelectionQueue.length=' +
479488
this.configSelectionQueue.length +
480489
' pickQueue.length=' +
481490
this.pickQueue.length
482491
);
483-
this.callRefTimer.unref?.();
492+
this.callRefTimer?.unref?.();
484493
}
485494
}
486495

@@ -509,12 +518,13 @@ export class InternalChannel {
509518
ConnectivityState[newState]
510519
);
511520
if (this.channelzEnabled) {
512-
this.channelzTrace.addTrace(
521+
this.channelzInfoTracker.trace.addTrace(
513522
'CT_INFO',
514523
'Connectivity state change to ' + ConnectivityState[newState]
515524
);
516525
}
517526
this.connectivityState = newState;
527+
this.channelzInfoTracker.state = newState;
518528
const watchersCopy = this.connectivityStateWatchers.slice();
519529
for (const watcherObject of watchersCopy) {
520530
if (newState !== watcherObject.currentState) {
@@ -539,6 +549,10 @@ export class InternalChannel {
539549
}
540550
}
541551

552+
addWrappedSubchannel(wrappedSubchannel: ChannelSubchannelWrapper) {
553+
this.wrappedSubchannels.add(wrappedSubchannel);
554+
}
555+
542556
removeWrappedSubchannel(wrappedSubchannel: ChannelSubchannelWrapper) {
543557
this.wrappedSubchannels.delete(wrappedSubchannel);
544558
}
@@ -591,6 +605,10 @@ export class InternalChannel {
591605
clearTimeout(this.idleTimer);
592606
this.idleTimer = null;
593607
}
608+
if (this.callRefTimer) {
609+
clearInterval(this.callRefTimer);
610+
this.callRefTimer = null;
611+
}
594612
}
595613

596614
private startIdleTimeout(timeoutMs: number) {
@@ -634,17 +652,17 @@ export class InternalChannel {
634652

635653
private onCallStart() {
636654
if (this.channelzEnabled) {
637-
this.callTracker.addCallStarted();
655+
this.channelzInfoTracker.callTracker.addCallStarted();
638656
}
639657
this.callCount += 1;
640658
}
641659

642660
private onCallEnd(status: StatusObject) {
643661
if (this.channelzEnabled) {
644662
if (status.code === Status.OK) {
645-
this.callTracker.addCallSucceeded();
663+
this.channelzInfoTracker.callTracker.addCallSucceeded();
646664
} else {
647-
this.callTracker.addCallFailed();
665+
this.channelzInfoTracker.callTracker.addCallFailed();
648666
}
649667
}
650668
this.callCount -= 1;
@@ -776,7 +794,9 @@ export class InternalChannel {
776794
call.cancelWithStatus(Status.UNAVAILABLE, 'Channel closed before call started');
777795
}
778796
this.pickQueue = [];
779-
clearInterval(this.callRefTimer);
797+
if (this.callRefTimer) {
798+
clearInterval(this.callRefTimer);
799+
}
780800
if (this.idleTimer) {
781801
clearTimeout(this.idleTimer);
782802
}

0 commit comments

Comments
 (0)