Skip to content

Commit 01c5ee7

Browse files
authored
Merge pull request #2272 from murgatroid99/grpc-js_max_connection_age
grpc-js: Implement server connection management
2 parents 6c0223d + a42d6b4 commit 01c5ee7

File tree

2 files changed

+76
-2
lines changed

2 files changed

+76
-2
lines changed

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ export interface ChannelOptions {
4444
'grpc.default_compression_algorithm'?: CompressionAlgorithms;
4545
'grpc.enable_channelz'?: number;
4646
'grpc.dns_min_time_between_resolutions_ms'?: number;
47+
'grpc.max_connection_age_ms'?: number;
48+
'grpc.max_connection_age_grace_ms'?: number;
4749
'grpc-node.max_session_memory'?: number;
4850
// eslint-disable-next-line @typescript-eslint/no-explicit-any
4951
[key: string]: any;
@@ -71,6 +73,8 @@ export const recognizedOptions = {
7173
'grpc.enable_http_proxy': true,
7274
'grpc.enable_channelz': true,
7375
'grpc.dns_min_time_between_resolutions_ms': true,
76+
'grpc.max_connection_age_ms': true,
77+
'grpc.max_connection_age_grace_ms': true,
7478
'grpc-node.max_session_memory': true,
7579
};
7680

packages/grpc-js/src/server.ts

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ import { ChannelzCallTracker, ChannelzChildrenTracker, ChannelzTrace, registerCh
6363
import { CipherNameAndProtocol, TLSSocket } from 'tls';
6464
import { getErrorCode, getErrorMessage } from './error';
6565

66+
const UNLIMITED_CONNECTION_AGE_MS = ~(1<<31);
67+
const KEEPALIVE_MAX_TIME_MS = ~(1<<31);
68+
const KEEPALIVE_TIMEOUT_MS = 20000;
69+
6670
const {
6771
HTTP2_HEADER_PATH
6872
} = http2.constants
@@ -161,6 +165,12 @@ export class Server {
161165
private listenerChildrenTracker = new ChannelzChildrenTracker();
162166
private sessionChildrenTracker = new ChannelzChildrenTracker();
163167

168+
private readonly maxConnectionAgeMs: number;
169+
private readonly maxConnectionAgeGraceMs: number;
170+
171+
private readonly keepaliveTimeMs: number;
172+
private readonly keepaliveTimeoutMs: number;
173+
164174
constructor(options?: ChannelOptions) {
165175
this.options = options ?? {};
166176
if (this.options['grpc.enable_channelz'] === 0) {
@@ -170,7 +180,10 @@ export class Server {
170180
if (this.channelzEnabled) {
171181
this.channelzTrace.addTrace('CT_INFO', 'Server created');
172182
}
173-
183+
this.maxConnectionAgeMs = this.options['grpc.max_connection_age_ms'] ?? UNLIMITED_CONNECTION_AGE_MS;
184+
this.maxConnectionAgeGraceMs = this.options['grpc.max_connection_age_grace_ms'] ?? UNLIMITED_CONNECTION_AGE_MS;
185+
this.keepaliveTimeMs = this.options['grpc.keepalive_time_ms'] ?? KEEPALIVE_MAX_TIME_MS;
186+
this.keepaliveTimeoutMs = this.options['grpc.keepalive_timeout_ms'] ?? KEEPALIVE_TIMEOUT_MS;
174187
this.trace('Server constructed');
175188
}
176189

@@ -970,12 +983,69 @@ export class Server {
970983
this.channelzTrace.addTrace('CT_INFO', 'Connection established by client ' + clientAddress);
971984
this.sessionChildrenTracker.refChild(channelzRef);
972985
}
986+
let connectionAgeTimer: NodeJS.Timer | null = null;
987+
let connectionAgeGraceTimer: NodeJS.Timer | null = null;
988+
let sessionClosedByServer = false;
989+
if (this.maxConnectionAgeMs !== UNLIMITED_CONNECTION_AGE_MS) {
990+
// Apply a random jitter within a +/-10% range
991+
const jitterMagnitude = this.maxConnectionAgeMs / 10;
992+
const jitter = Math.random() * jitterMagnitude * 2 - jitterMagnitude;
993+
connectionAgeTimer = setTimeout(() => {
994+
sessionClosedByServer = true;
995+
if (this.channelzEnabled) {
996+
this.channelzTrace.addTrace('CT_INFO', 'Connection dropped by max connection age from ' + clientAddress);
997+
}
998+
try {
999+
session.goaway(http2.constants.NGHTTP2_NO_ERROR, ~(1<<31), Buffer.from('max_age'));
1000+
} catch (e) {
1001+
// The goaway can't be sent because the session is already closed
1002+
session.destroy();
1003+
return;
1004+
}
1005+
session.close();
1006+
/* Allow a grace period after sending the GOAWAY before forcibly
1007+
* closing the connection. */
1008+
if (this.maxConnectionAgeGraceMs !== UNLIMITED_CONNECTION_AGE_MS) {
1009+
connectionAgeGraceTimer = setTimeout(() => {
1010+
session.destroy();
1011+
}, this.maxConnectionAgeGraceMs).unref?.();
1012+
}
1013+
}, this.maxConnectionAgeMs + jitter).unref?.();
1014+
}
1015+
const keeapliveTimeTimer: NodeJS.Timer | null = setInterval(() => {
1016+
const timeoutTImer = setTimeout(() => {
1017+
sessionClosedByServer = true;
1018+
if (this.channelzEnabled) {
1019+
this.channelzTrace.addTrace('CT_INFO', 'Connection dropped by keepalive timeout from ' + clientAddress);
1020+
}
1021+
session.close();
1022+
}, this.keepaliveTimeoutMs).unref?.();
1023+
try {
1024+
session.ping((err: Error | null, duration: number, payload: Buffer) => {
1025+
clearTimeout(timeoutTImer);
1026+
});
1027+
} catch (e) {
1028+
// The ping can't be sent because the session is already closed
1029+
session.destroy();
1030+
}
1031+
}, this.keepaliveTimeMs).unref?.();
9731032
session.on('close', () => {
9741033
if (this.channelzEnabled) {
975-
this.channelzTrace.addTrace('CT_INFO', 'Connection dropped by client ' + clientAddress);
1034+
if (!sessionClosedByServer) {
1035+
this.channelzTrace.addTrace('CT_INFO', 'Connection dropped by client ' + clientAddress);
1036+
}
9761037
this.sessionChildrenTracker.unrefChild(channelzRef);
9771038
unregisterChannelzRef(channelzRef);
9781039
}
1040+
if (connectionAgeTimer) {
1041+
clearTimeout(connectionAgeTimer);
1042+
}
1043+
if (connectionAgeGraceTimer) {
1044+
clearTimeout(connectionAgeGraceTimer);
1045+
}
1046+
if (keeapliveTimeTimer) {
1047+
clearTimeout(keeapliveTimeTimer);
1048+
}
9791049
this.sessions.delete(session);
9801050
});
9811051
});

0 commit comments

Comments
 (0)