1 | "use strict";
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 |
|
18 | Object.defineProperty(exports, "__esModule", { value: true });
|
19 | exports.Subchannel = exports.subchannelAddressToString = exports.subchannelAddressEqual = exports.isTcpSubchannelAddress = void 0;
|
20 | const http2 = require("http2");
|
21 | const tls_1 = require("tls");
|
22 | const channel_1 = require("./channel");
|
23 | const backoff_timeout_1 = require("./backoff-timeout");
|
24 | const resolver_1 = require("./resolver");
|
25 | const logging = require("./logging");
|
26 | const constants_1 = require("./constants");
|
27 | const http_proxy_1 = require("./http_proxy");
|
28 | const net = require("net");
|
29 | const uri_parser_1 = require("./uri-parser");
|
30 | const clientVersion = require('../../package.json').version;
|
31 | const TRACER_NAME = 'subchannel';
|
32 | function trace(text) {
|
33 | logging.trace(constants_1.LogVerbosity.DEBUG, TRACER_NAME, text);
|
34 | }
|
35 | function refTrace(text) {
|
36 | logging.trace(constants_1.LogVerbosity.DEBUG, 'subchannel_refcount', text);
|
37 | }
|
38 | const MIN_CONNECT_TIMEOUT_MS = 20000;
|
39 | const INITIAL_BACKOFF_MS = 1000;
|
40 | const BACKOFF_MULTIPLIER = 1.6;
|
41 | const MAX_BACKOFF_MS = 120000;
|
42 | const BACKOFF_JITTER = 0.2;
|
43 |
|
44 |
|
45 |
|
46 | const KEEPALIVE_MAX_TIME_MS = ~(1 << 31);
|
47 | const KEEPALIVE_TIMEOUT_MS = 20000;
|
48 | const { HTTP2_HEADER_AUTHORITY, HTTP2_HEADER_CONTENT_TYPE, HTTP2_HEADER_METHOD, HTTP2_HEADER_PATH, HTTP2_HEADER_TE, HTTP2_HEADER_USER_AGENT, } = http2.constants;
|
49 |
|
50 |
|
51 |
|
52 |
|
53 |
|
54 | function uniformRandom(min, max) {
|
55 | return Math.random() * (max - min) + min;
|
56 | }
|
57 | const tooManyPingsData = Buffer.from('too_many_pings', 'ascii');
|
58 | function isTcpSubchannelAddress(address) {
|
59 | return 'port' in address;
|
60 | }
|
61 | exports.isTcpSubchannelAddress = isTcpSubchannelAddress;
|
62 | function subchannelAddressEqual(address1, address2) {
|
63 | if (isTcpSubchannelAddress(address1)) {
|
64 | return (isTcpSubchannelAddress(address2) &&
|
65 | address1.host === address2.host &&
|
66 | address1.port === address2.port);
|
67 | }
|
68 | else {
|
69 | return !isTcpSubchannelAddress(address2) && address1.path === address2.path;
|
70 | }
|
71 | }
|
72 | exports.subchannelAddressEqual = subchannelAddressEqual;
|
73 | function subchannelAddressToString(address) {
|
74 | if (isTcpSubchannelAddress(address)) {
|
75 | return address.host + ':' + address.port;
|
76 | }
|
77 | else {
|
78 | return address.path;
|
79 | }
|
80 | }
|
81 | exports.subchannelAddressToString = subchannelAddressToString;
|
82 | class Subchannel {
|
83 | |
84 |
|
85 |
|
86 |
|
87 |
|
88 |
|
89 |
|
90 |
|
91 |
|
92 |
|
93 | constructor(channelTarget, subchannelAddress, options, credentials) {
|
94 | this.channelTarget = channelTarget;
|
95 | this.subchannelAddress = subchannelAddress;
|
96 | this.options = options;
|
97 | this.credentials = credentials;
|
98 | |
99 |
|
100 |
|
101 |
|
102 | this.connectivityState = channel_1.ConnectivityState.IDLE;
|
103 | |
104 |
|
105 |
|
106 | this.session = null;
|
107 | |
108 |
|
109 |
|
110 |
|
111 | this.continueConnecting = false;
|
112 | |
113 |
|
114 |
|
115 |
|
116 |
|
117 | this.stateListeners = [];
|
118 | |
119 |
|
120 |
|
121 |
|
122 |
|
123 | this.disconnectListeners = [];
|
124 | |
125 |
|
126 |
|
127 | this.keepaliveTimeMs = KEEPALIVE_MAX_TIME_MS;
|
128 | |
129 |
|
130 |
|
131 | this.keepaliveTimeoutMs = KEEPALIVE_TIMEOUT_MS;
|
132 | |
133 |
|
134 |
|
135 | this.keepaliveWithoutCalls = false;
|
136 | |
137 |
|
138 |
|
139 | this.callRefcount = 0;
|
140 | |
141 |
|
142 |
|
143 | this.refcount = 0;
|
144 |
|
145 | this.userAgent = [
|
146 | options['grpc.primary_user_agent'],
|
147 | `grpc-node-js/${clientVersion}`,
|
148 | options['grpc.secondary_user_agent'],
|
149 | ]
|
150 | .filter((e) => e)
|
151 | .join(' ');
|
152 | if ('grpc.keepalive_time_ms' in options) {
|
153 | this.keepaliveTimeMs = options['grpc.keepalive_time_ms'];
|
154 | }
|
155 | if ('grpc.keepalive_timeout_ms' in options) {
|
156 | this.keepaliveTimeoutMs = options['grpc.keepalive_timeout_ms'];
|
157 | }
|
158 | if ('grpc.keepalive_permit_without_calls' in options) {
|
159 | this.keepaliveWithoutCalls = options['grpc.keepalive_permit_without_calls'] === 1;
|
160 | }
|
161 | else {
|
162 | this.keepaliveWithoutCalls = false;
|
163 | }
|
164 | this.keepaliveIntervalId = setTimeout(() => { }, 0);
|
165 | clearTimeout(this.keepaliveIntervalId);
|
166 | this.keepaliveTimeoutId = setTimeout(() => { }, 0);
|
167 | clearTimeout(this.keepaliveTimeoutId);
|
168 | const backoffOptions = {
|
169 | initialDelay: options['grpc.initial_reconnect_backoff_ms'],
|
170 | maxDelay: options['grpc.max_reconnect_backoff_ms'],
|
171 | };
|
172 | this.backoffTimeout = new backoff_timeout_1.BackoffTimeout(() => {
|
173 | this.handleBackoffTimer();
|
174 | }, backoffOptions);
|
175 | this.subchannelAddressString = subchannelAddressToString(subchannelAddress);
|
176 | }
|
177 | handleBackoffTimer() {
|
178 | if (this.continueConnecting) {
|
179 | this.transitionToState([channel_1.ConnectivityState.TRANSIENT_FAILURE], channel_1.ConnectivityState.CONNECTING);
|
180 | }
|
181 | else {
|
182 | this.transitionToState([channel_1.ConnectivityState.TRANSIENT_FAILURE], channel_1.ConnectivityState.IDLE);
|
183 | }
|
184 | }
|
185 | |
186 |
|
187 |
|
188 | startBackoff() {
|
189 | this.backoffTimeout.runOnce();
|
190 | }
|
191 | stopBackoff() {
|
192 | this.backoffTimeout.stop();
|
193 | this.backoffTimeout.reset();
|
194 | }
|
195 | sendPing() {
|
196 | var _a, _b;
|
197 | logging.trace(constants_1.LogVerbosity.DEBUG, 'keepalive', 'Sending ping to ' + this.subchannelAddressString);
|
198 | this.keepaliveTimeoutId = setTimeout(() => {
|
199 | this.transitionToState([channel_1.ConnectivityState.READY], channel_1.ConnectivityState.IDLE);
|
200 | }, this.keepaliveTimeoutMs);
|
201 | (_b = (_a = this.keepaliveTimeoutId).unref) === null || _b === void 0 ? void 0 : _b.call(_a);
|
202 | this.session.ping((err, duration, payload) => {
|
203 | clearTimeout(this.keepaliveTimeoutId);
|
204 | });
|
205 | }
|
206 | startKeepalivePings() {
|
207 | var _a, _b;
|
208 | this.keepaliveIntervalId = setInterval(() => {
|
209 | this.sendPing();
|
210 | }, this.keepaliveTimeMs);
|
211 | (_b = (_a = this.keepaliveIntervalId).unref) === null || _b === void 0 ? void 0 : _b.call(_a);
|
212 | |
213 |
|
214 | }
|
215 | stopKeepalivePings() {
|
216 | clearInterval(this.keepaliveIntervalId);
|
217 | clearTimeout(this.keepaliveTimeoutId);
|
218 | }
|
219 | createSession(proxyConnectionResult) {
|
220 | var _a, _b, _c;
|
221 | if (proxyConnectionResult.realTarget) {
|
222 | trace(this.subchannelAddressString + ' creating HTTP/2 session through proxy to ' + proxyConnectionResult.realTarget);
|
223 | }
|
224 | else {
|
225 | trace(this.subchannelAddressString + ' creating HTTP/2 session');
|
226 | }
|
227 | const targetAuthority = resolver_1.getDefaultAuthority((_a = proxyConnectionResult.realTarget) !== null && _a !== void 0 ? _a : this.channelTarget);
|
228 | let connectionOptions = this.credentials._getConnectionOptions() || {};
|
229 | connectionOptions.maxSendHeaderBlockLength = Number.MAX_SAFE_INTEGER;
|
230 | if ('grpc-node.max_session_memory' in this.options) {
|
231 | connectionOptions.maxSessionMemory = this.options['grpc-node.max_session_memory'];
|
232 | }
|
233 | let addressScheme = 'http://';
|
234 | if ('secureContext' in connectionOptions) {
|
235 | addressScheme = 'https://';
|
236 |
|
237 |
|
238 |
|
239 | if (this.options['grpc.ssl_target_name_override']) {
|
240 | const sslTargetNameOverride = this.options['grpc.ssl_target_name_override'];
|
241 | connectionOptions.checkServerIdentity = (host, cert) => {
|
242 | return tls_1.checkServerIdentity(sslTargetNameOverride, cert);
|
243 | };
|
244 | connectionOptions.servername = sslTargetNameOverride;
|
245 | }
|
246 | else {
|
247 | const authorityHostname = (_c = (_b = uri_parser_1.splitHostPort(targetAuthority)) === null || _b === void 0 ? void 0 : _b.host) !== null && _c !== void 0 ? _c : 'localhost';
|
248 |
|
249 | connectionOptions.servername = authorityHostname;
|
250 | }
|
251 | if (proxyConnectionResult.socket) {
|
252 | |
253 |
|
254 |
|
255 |
|
256 |
|
257 | connectionOptions.createConnection = (authority, option) => {
|
258 | return proxyConnectionResult.socket;
|
259 | };
|
260 | }
|
261 | }
|
262 | else {
|
263 | |
264 |
|
265 |
|
266 | connectionOptions.createConnection = (authority, option) => {
|
267 | if (proxyConnectionResult.socket) {
|
268 | return proxyConnectionResult.socket;
|
269 | }
|
270 | else {
|
271 | |
272 |
|
273 |
|
274 | return net.connect(this.subchannelAddress);
|
275 | }
|
276 | };
|
277 | }
|
278 | connectionOptions = Object.assign(Object.assign({}, connectionOptions), this.subchannelAddress);
|
279 | |
280 |
|
281 |
|
282 |
|
283 |
|
284 |
|
285 |
|
286 |
|
287 |
|
288 |
|
289 |
|
290 |
|
291 |
|
292 |
|
293 |
|
294 |
|
295 |
|
296 | const session = http2.connect(addressScheme + targetAuthority, connectionOptions);
|
297 | this.session = session;
|
298 | session.unref();
|
299 | |
300 |
|
301 |
|
302 |
|
303 | session.once('connect', () => {
|
304 | if (this.session === session) {
|
305 | this.transitionToState([channel_1.ConnectivityState.CONNECTING], channel_1.ConnectivityState.READY);
|
306 | }
|
307 | });
|
308 | session.once('close', () => {
|
309 | if (this.session === session) {
|
310 | trace(this.subchannelAddressString + ' connection closed');
|
311 | this.transitionToState([channel_1.ConnectivityState.CONNECTING], channel_1.ConnectivityState.TRANSIENT_FAILURE);
|
312 | |
313 |
|
314 |
|
315 | this.transitionToState([channel_1.ConnectivityState.READY], channel_1.ConnectivityState.IDLE);
|
316 | }
|
317 | });
|
318 | session.once('goaway', (errorCode, lastStreamID, opaqueData) => {
|
319 | if (this.session === session) {
|
320 | |
321 |
|
322 | if (errorCode === http2.constants.NGHTTP2_ENHANCE_YOUR_CALM &&
|
323 | opaqueData.equals(tooManyPingsData)) {
|
324 | this.keepaliveTimeMs = Math.min(2 * this.keepaliveTimeMs, KEEPALIVE_MAX_TIME_MS);
|
325 | logging.log(constants_1.LogVerbosity.ERROR, `Connection to ${uri_parser_1.uriToString(this.channelTarget)} at ${this.subchannelAddressString} rejected by server because of excess pings. Increasing ping interval to ${this.keepaliveTimeMs} ms`);
|
326 | }
|
327 | trace(this.subchannelAddressString +
|
328 | ' connection closed by GOAWAY with code ' +
|
329 | errorCode);
|
330 | this.transitionToState([channel_1.ConnectivityState.CONNECTING, channel_1.ConnectivityState.READY], channel_1.ConnectivityState.IDLE);
|
331 | }
|
332 | });
|
333 | session.once('error', (error) => {
|
334 | |
335 |
|
336 | trace(this.subchannelAddressString +
|
337 | ' connection closed with error ' +
|
338 | error.message);
|
339 | });
|
340 | }
|
341 | startConnectingInternal() {
|
342 | var _a, _b;
|
343 | |
344 |
|
345 |
|
346 |
|
347 | const connectionOptions = this.credentials._getConnectionOptions() || {};
|
348 | if ('secureContext' in connectionOptions) {
|
349 | connectionOptions.ALPNProtocols = ['h2'];
|
350 |
|
351 |
|
352 |
|
353 | if (this.options['grpc.ssl_target_name_override']) {
|
354 | const sslTargetNameOverride = this.options['grpc.ssl_target_name_override'];
|
355 | connectionOptions.checkServerIdentity = (host, cert) => {
|
356 | return tls_1.checkServerIdentity(sslTargetNameOverride, cert);
|
357 | };
|
358 | connectionOptions.servername = sslTargetNameOverride;
|
359 | }
|
360 | else {
|
361 | if ('grpc.http_connect_target' in this.options) {
|
362 | |
363 |
|
364 |
|
365 |
|
366 | const targetPath = resolver_1.getDefaultAuthority((_a = uri_parser_1.parseUri(this.options['grpc.http_connect_target'])) !== null && _a !== void 0 ? _a : {
|
367 | path: 'localhost',
|
368 | });
|
369 | const hostPort = uri_parser_1.splitHostPort(targetPath);
|
370 | connectionOptions.servername = (_b = hostPort === null || hostPort === void 0 ? void 0 : hostPort.host) !== null && _b !== void 0 ? _b : targetPath;
|
371 | }
|
372 | }
|
373 | }
|
374 | http_proxy_1.getProxiedConnection(this.subchannelAddress, this.options, connectionOptions).then((result) => {
|
375 | this.createSession(result);
|
376 | }, (reason) => {
|
377 | this.transitionToState([channel_1.ConnectivityState.CONNECTING], channel_1.ConnectivityState.TRANSIENT_FAILURE);
|
378 | });
|
379 | }
|
380 | |
381 |
|
382 |
|
383 |
|
384 |
|
385 |
|
386 |
|
387 | transitionToState(oldStates, newState) {
|
388 | if (oldStates.indexOf(this.connectivityState) === -1) {
|
389 | return false;
|
390 | }
|
391 | trace(this.subchannelAddressString +
|
392 | ' ' +
|
393 | channel_1.ConnectivityState[this.connectivityState] +
|
394 | ' -> ' +
|
395 | channel_1.ConnectivityState[newState]);
|
396 | const previousState = this.connectivityState;
|
397 | this.connectivityState = newState;
|
398 | switch (newState) {
|
399 | case channel_1.ConnectivityState.READY:
|
400 | this.stopBackoff();
|
401 | this.session.socket.once('close', () => {
|
402 | for (const listener of this.disconnectListeners) {
|
403 | listener();
|
404 | }
|
405 | });
|
406 | if (this.keepaliveWithoutCalls) {
|
407 | this.startKeepalivePings();
|
408 | }
|
409 | break;
|
410 | case channel_1.ConnectivityState.CONNECTING:
|
411 | this.startBackoff();
|
412 | this.startConnectingInternal();
|
413 | this.continueConnecting = false;
|
414 | break;
|
415 | case channel_1.ConnectivityState.TRANSIENT_FAILURE:
|
416 | if (this.session) {
|
417 | this.session.close();
|
418 | }
|
419 | this.session = null;
|
420 | this.stopKeepalivePings();
|
421 | |
422 |
|
423 |
|
424 | if (!this.backoffTimeout.isRunning()) {
|
425 | process.nextTick(() => {
|
426 | this.handleBackoffTimer();
|
427 | });
|
428 | }
|
429 | break;
|
430 | case channel_1.ConnectivityState.IDLE:
|
431 | if (this.session) {
|
432 | this.session.close();
|
433 | }
|
434 | this.session = null;
|
435 | this.stopKeepalivePings();
|
436 | break;
|
437 | default:
|
438 | throw new Error(`Invalid state: unknown ConnectivityState ${newState}`);
|
439 | }
|
440 | |
441 |
|
442 | for (const listener of [...this.stateListeners]) {
|
443 | listener(this, previousState, newState);
|
444 | }
|
445 | return true;
|
446 | }
|
447 | |
448 |
|
449 |
|
450 |
|
451 | checkBothRefcounts() {
|
452 | |
453 |
|
454 | if (this.callRefcount === 0 && this.refcount === 0) {
|
455 | this.transitionToState([
|
456 | channel_1.ConnectivityState.CONNECTING,
|
457 | channel_1.ConnectivityState.READY,
|
458 | ], channel_1.ConnectivityState.TRANSIENT_FAILURE);
|
459 | }
|
460 | }
|
461 | callRef() {
|
462 | refTrace(this.subchannelAddressString +
|
463 | ' callRefcount ' +
|
464 | this.callRefcount +
|
465 | ' -> ' +
|
466 | (this.callRefcount + 1));
|
467 | if (this.callRefcount === 0) {
|
468 | if (this.session) {
|
469 | this.session.ref();
|
470 | }
|
471 | this.backoffTimeout.ref();
|
472 | if (!this.keepaliveWithoutCalls) {
|
473 | this.startKeepalivePings();
|
474 | }
|
475 | }
|
476 | this.callRefcount += 1;
|
477 | }
|
478 | callUnref() {
|
479 | refTrace(this.subchannelAddressString +
|
480 | ' callRefcount ' +
|
481 | this.callRefcount +
|
482 | ' -> ' +
|
483 | (this.callRefcount - 1));
|
484 | this.callRefcount -= 1;
|
485 | if (this.callRefcount === 0) {
|
486 | if (this.session) {
|
487 | this.session.unref();
|
488 | }
|
489 | this.backoffTimeout.unref();
|
490 | if (!this.keepaliveWithoutCalls) {
|
491 | this.stopKeepalivePings();
|
492 | }
|
493 | this.checkBothRefcounts();
|
494 | }
|
495 | }
|
496 | ref() {
|
497 | refTrace(this.subchannelAddressString +
|
498 | ' refcount ' +
|
499 | this.refcount +
|
500 | ' -> ' +
|
501 | (this.refcount + 1));
|
502 | this.refcount += 1;
|
503 | }
|
504 | unref() {
|
505 | refTrace(this.subchannelAddressString +
|
506 | ' refcount ' +
|
507 | this.refcount +
|
508 | ' -> ' +
|
509 | (this.refcount - 1));
|
510 | this.refcount -= 1;
|
511 | this.checkBothRefcounts();
|
512 | }
|
513 | unrefIfOneRef() {
|
514 | if (this.refcount === 1) {
|
515 | this.unref();
|
516 | return true;
|
517 | }
|
518 | return false;
|
519 | }
|
520 | |
521 |
|
522 |
|
523 |
|
524 |
|
525 |
|
526 |
|
527 | startCallStream(metadata, callStream, extraFilterFactory) {
|
528 | const headers = metadata.toHttp2Headers();
|
529 | headers[HTTP2_HEADER_AUTHORITY] = callStream.getHost();
|
530 | headers[HTTP2_HEADER_USER_AGENT] = this.userAgent;
|
531 | headers[HTTP2_HEADER_CONTENT_TYPE] = 'application/grpc';
|
532 | headers[HTTP2_HEADER_METHOD] = 'POST';
|
533 | headers[HTTP2_HEADER_PATH] = callStream.getMethod();
|
534 | headers[HTTP2_HEADER_TE] = 'trailers';
|
535 | let http2Stream;
|
536 | |
537 |
|
538 |
|
539 |
|
540 |
|
541 |
|
542 |
|
543 |
|
544 | try {
|
545 | http2Stream = this.session.request(headers);
|
546 | }
|
547 | catch (e) {
|
548 | this.transitionToState([channel_1.ConnectivityState.READY], channel_1.ConnectivityState.TRANSIENT_FAILURE);
|
549 | throw e;
|
550 | }
|
551 | let headersString = '';
|
552 | for (const header of Object.keys(headers)) {
|
553 | headersString += '\t\t' + header + ': ' + headers[header] + '\n';
|
554 | }
|
555 | logging.trace(constants_1.LogVerbosity.DEBUG, 'call_stream', 'Starting stream on subchannel ' + this.subchannelAddressString + ' with headers\n' + headersString);
|
556 | callStream.attachHttp2Stream(http2Stream, this, extraFilterFactory);
|
557 | }
|
558 | |
559 |
|
560 |
|
561 |
|
562 |
|
563 |
|
564 | startConnecting() {
|
565 | |
566 |
|
567 |
|
568 |
|
569 | if (!this.transitionToState([channel_1.ConnectivityState.IDLE], channel_1.ConnectivityState.CONNECTING)) {
|
570 | if (this.connectivityState === channel_1.ConnectivityState.TRANSIENT_FAILURE) {
|
571 | this.continueConnecting = true;
|
572 | }
|
573 | }
|
574 | }
|
575 | |
576 |
|
577 |
|
578 | getConnectivityState() {
|
579 | return this.connectivityState;
|
580 | }
|
581 | |
582 |
|
583 |
|
584 |
|
585 |
|
586 | addConnectivityStateListener(listener) {
|
587 | this.stateListeners.push(listener);
|
588 | }
|
589 | |
590 |
|
591 |
|
592 |
|
593 |
|
594 | removeConnectivityStateListener(listener) {
|
595 | const listenerIndex = this.stateListeners.indexOf(listener);
|
596 | if (listenerIndex > -1) {
|
597 | this.stateListeners.splice(listenerIndex, 1);
|
598 | }
|
599 | }
|
600 | addDisconnectListener(listener) {
|
601 | this.disconnectListeners.push(listener);
|
602 | }
|
603 | removeDisconnectListener(listener) {
|
604 | const listenerIndex = this.disconnectListeners.indexOf(listener);
|
605 | if (listenerIndex > -1) {
|
606 | this.disconnectListeners.splice(listenerIndex, 1);
|
607 | }
|
608 | }
|
609 | |
610 |
|
611 |
|
612 | resetBackoff() {
|
613 | this.backoffTimeout.reset();
|
614 | this.transitionToState([channel_1.ConnectivityState.TRANSIENT_FAILURE], channel_1.ConnectivityState.CONNECTING);
|
615 | }
|
616 | getAddress() {
|
617 | return this.subchannelAddressString;
|
618 | }
|
619 | }
|
620 | exports.Subchannel = Subchannel;
|
621 |
|
\ | No newline at end of file |