Skip to content

Commit

Permalink
grpc-js: Fix and optimize IDLE timeouts
Browse files Browse the repository at this point in the history
  • Loading branch information
murgatroid99 committed Jan 16, 2024
1 parent 442e3ea commit 6da0b49
Show file tree
Hide file tree
Showing 2 changed files with 35 additions and 10 deletions.
2 changes: 1 addition & 1 deletion packages/grpc-js/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@grpc/grpc-js",
"version": "1.9.13",
"version": "1.9.14",
"description": "gRPC Library for Node - pure JS implementation",
"homepage": "https://grpc.io/",
"repository": "https://github.com/grpc/grpc-node/tree/master/packages/grpc-js",
Expand Down
43 changes: 34 additions & 9 deletions packages/grpc-js/src/internal-channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ export class InternalChannel {
private callCount = 0;
private idleTimer: NodeJS.Timeout | null = null;
private readonly idleTimeoutMs: number;
private lastActivityTimestamp: Date;

// Channelz info
private readonly channelzEnabled: boolean = true;
Expand Down Expand Up @@ -409,6 +410,7 @@ export class InternalChannel {
'Channel constructed \n' +
error.stack?.substring(error.stack.indexOf('\n') + 1)
);
this.lastActivityTimestamp = new Date();
}

private getChannelzInfo(): ChannelInfo {
Expand Down Expand Up @@ -556,19 +558,44 @@ export class InternalChannel {
this.resolvingLoadBalancer.destroy();
this.updateState(ConnectivityState.IDLE);
this.currentPicker = new QueuePicker(this.resolvingLoadBalancer);
if (this.idleTimer) {
clearTimeout(this.idleTimer);
this.idleTimer = null;
}
}

private maybeStartIdleTimer() {
if (this.connectivityState !== ConnectivityState.SHUTDOWN && this.callCount === 0) {
this.idleTimer = setTimeout(() => {
private startIdleTimeout(timeoutMs: number) {
this.idleTimer = setTimeout(() => {
if (this.callCount > 0) {
/* If there is currently a call, the channel will not go idle for a
* period of at least idleTimeoutMs, so check again after that time.
*/
this.startIdleTimeout(this.idleTimeoutMs);
return;
}
const now = new Date();
const timeSinceLastActivity = now.valueOf() - this.lastActivityTimestamp.valueOf();
if (timeSinceLastActivity >= this.idleTimeoutMs) {
this.trace(
'Idle timer triggered after ' +
this.idleTimeoutMs +
'ms of inactivity'
);
this.enterIdle();
}, this.idleTimeoutMs);
this.idleTimer.unref?.();
} else {
/* Whenever the timer fires with the latest activity being too recent,
* set the timer again for the time when the time since the last
* activity is equal to the timeout. This should result in the timer
* firing no more than once every idleTimeoutMs/2 on average. */
this.startIdleTimeout(this.idleTimeoutMs - timeSinceLastActivity);
}
}, timeoutMs);
this.idleTimer.unref?.();
}

private maybeStartIdleTimer() {
if (this.connectivityState !== ConnectivityState.SHUTDOWN && !this.idleTimer) {
this.startIdleTimeout(this.idleTimeoutMs);
}
}

Expand All @@ -577,10 +604,6 @@ export class InternalChannel {
this.callTracker.addCallStarted();
}
this.callCount += 1;
if (this.idleTimer) {
clearTimeout(this.idleTimer);
this.idleTimer = null;
}
}

private onCallEnd(status: StatusObject) {
Expand All @@ -592,6 +615,7 @@ export class InternalChannel {
}
}
this.callCount -= 1;
this.lastActivityTimestamp = new Date();
this.maybeStartIdleTimer();
}

Expand Down Expand Up @@ -729,6 +753,7 @@ export class InternalChannel {
const connectivityState = this.connectivityState;
if (tryToConnect) {
this.resolvingLoadBalancer.exitIdle();
this.lastActivityTimestamp = new Date();
this.maybeStartIdleTimer();
}
return connectivityState;
Expand Down

0 comments on commit 6da0b49

Please sign in to comment.