UNPKG

23.8 kBPlain TextView Raw
1/*
2 * Copyright 2019 gRPC authors.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 *
16 */
17
18import { ChannelCredentials } from './channel-credentials';
19import { ChannelOptions } from './channel-options';
20import { ResolvingLoadBalancer } from './resolving-load-balancer';
21import { SubchannelPool, getSubchannelPool } from './subchannel-pool';
22import { ChannelControlHelper } from './load-balancer';
23import { UnavailablePicker, Picker, QueuePicker } from './picker';
24import { Metadata } from './metadata';
25import { Status, LogVerbosity, Propagate } from './constants';
26import { FilterStackFactory } from './filter-stack';
27import { CompressionFilterFactory } from './compression-filter';
28import {
29 CallConfig,
30 ConfigSelector,
31 getDefaultAuthority,
32 mapUriDefaultScheme,
33} from './resolver';
34import { trace } from './logging';
35import { SubchannelAddress } from './subchannel-address';
36import { MaxMessageSizeFilterFactory } from './max-message-size-filter';
37import { mapProxyName } from './http_proxy';
38import { GrpcUri, parseUri, uriToString } from './uri-parser';
39import { ServerSurfaceCall } from './server-call';
40
41import { ConnectivityState } from './connectivity-state';
42import {
43 ChannelInfo,
44 ChannelRef,
45 ChannelzCallTracker,
46 ChannelzChildrenTracker,
47 ChannelzTrace,
48 registerChannelzChannel,
49 SubchannelRef,
50 unregisterChannelzRef,
51} from './channelz';
52import { LoadBalancingCall } from './load-balancing-call';
53import { CallCredentials } from './call-credentials';
54import { Call, CallStreamOptions, StatusObject } from './call-interface';
55import { Deadline, deadlineToString } from './deadline';
56import { ResolvingCall } from './resolving-call';
57import { getNextCallNumber } from './call-number';
58import { restrictControlPlaneStatusCode } from './control-plane-status';
59import {
60 MessageBufferTracker,
61 RetryingCall,
62 RetryThrottler,
63} from './retrying-call';
64import {
65 BaseSubchannelWrapper,
66 ConnectivityStateListener,
67 SubchannelInterface,
68} from './subchannel-interface';
69
70/**
71 * See https://nodejs.org/api/timers.html#timers_setinterval_callback_delay_args
72 */
73const MAX_TIMEOUT_TIME = 2147483647;
74
75const MIN_IDLE_TIMEOUT_MS = 1000;
76
77// 30 minutes
78const DEFAULT_IDLE_TIMEOUT_MS = 30 * 60 * 1000;
79
80interface ConnectivityStateWatcher {
81 currentState: ConnectivityState;
82 timer: NodeJS.Timeout | null;
83 callback: (error?: Error) => void;
84}
85
86interface NoneConfigResult {
87 type: 'NONE';
88}
89
90interface SuccessConfigResult {
91 type: 'SUCCESS';
92 config: CallConfig;
93}
94
95interface ErrorConfigResult {
96 type: 'ERROR';
97 error: StatusObject;
98}
99
100type GetConfigResult =
101 | NoneConfigResult
102 | SuccessConfigResult
103 | ErrorConfigResult;
104
105const RETRY_THROTTLER_MAP: Map<string, RetryThrottler> = new Map();
106
107const DEFAULT_RETRY_BUFFER_SIZE_BYTES = 1 << 24; // 16 MB
108const DEFAULT_PER_RPC_RETRY_BUFFER_SIZE_BYTES = 1 << 20; // 1 MB
109
110class ChannelSubchannelWrapper
111 extends BaseSubchannelWrapper
112 implements SubchannelInterface
113{
114 private refCount = 0;
115 private subchannelStateListener: ConnectivityStateListener;
116 constructor(
117 childSubchannel: SubchannelInterface,
118 private channel: InternalChannel
119 ) {
120 super(childSubchannel);
121 this.subchannelStateListener = (
122 subchannel,
123 previousState,
124 newState,
125 keepaliveTime
126 ) => {
127 channel.throttleKeepalive(keepaliveTime);
128 };
129 childSubchannel.addConnectivityStateListener(this.subchannelStateListener);
130 }
131
132 ref(): void {
133 this.child.ref();
134 this.refCount += 1;
135 }
136
137 unref(): void {
138 this.child.unref();
139 this.refCount -= 1;
140 if (this.refCount <= 0) {
141 this.child.removeConnectivityStateListener(this.subchannelStateListener);
142 this.channel.removeWrappedSubchannel(this);
143 }
144 }
145}
146
147export class InternalChannel {
148 private readonly resolvingLoadBalancer: ResolvingLoadBalancer;
149 private readonly subchannelPool: SubchannelPool;
150 private connectivityState: ConnectivityState = ConnectivityState.IDLE;
151 private currentPicker: Picker = new UnavailablePicker();
152 /**
153 * Calls queued up to get a call config. Should only be populated before the
154 * first time the resolver returns a result, which includes the ConfigSelector.
155 */
156 private configSelectionQueue: ResolvingCall[] = [];
157 private pickQueue: LoadBalancingCall[] = [];
158 private connectivityStateWatchers: ConnectivityStateWatcher[] = [];
159 private readonly defaultAuthority: string;
160 private readonly filterStackFactory: FilterStackFactory;
161 private readonly target: GrpcUri;
162 /**
163 * This timer does not do anything on its own. Its purpose is to hold the
164 * event loop open while there are any pending calls for the channel that
165 * have not yet been assigned to specific subchannels. In other words,
166 * the invariant is that callRefTimer is reffed if and only if pickQueue
167 * is non-empty.
168 */
169 private readonly callRefTimer: NodeJS.Timeout;
170 private configSelector: ConfigSelector | null = null;
171 /**
172 * This is the error from the name resolver if it failed most recently. It
173 * is only used to end calls that start while there is no config selector
174 * and the name resolver is in backoff, so it should be nulled if
175 * configSelector becomes set or the channel state becomes anything other
176 * than TRANSIENT_FAILURE.
177 */
178 private currentResolutionError: StatusObject | null = null;
179 private readonly retryBufferTracker: MessageBufferTracker;
180 private keepaliveTime: number;
181 private readonly wrappedSubchannels: Set<ChannelSubchannelWrapper> =
182 new Set();
183
184 private callCount = 0;
185 private idleTimer: NodeJS.Timeout | null = null;
186 private readonly idleTimeoutMs: number;
187
188 // Channelz info
189 private readonly channelzEnabled: boolean = true;
190 private readonly originalTarget: string;
191 private readonly channelzRef: ChannelRef;
192 private readonly channelzTrace: ChannelzTrace;
193 private readonly callTracker = new ChannelzCallTracker();
194 private readonly childrenTracker = new ChannelzChildrenTracker();
195
196 constructor(
197 target: string,
198 private readonly credentials: ChannelCredentials,
199 private readonly options: ChannelOptions
200 ) {
201 if (typeof target !== 'string') {
202 throw new TypeError('Channel target must be a string');
203 }
204 if (!(credentials instanceof ChannelCredentials)) {
205 throw new TypeError(
206 'Channel credentials must be a ChannelCredentials object'
207 );
208 }
209 if (options) {
210 if (typeof options !== 'object') {
211 throw new TypeError('Channel options must be an object');
212 }
213 }
214 this.originalTarget = target;
215 const originalTargetUri = parseUri(target);
216 if (originalTargetUri === null) {
217 throw new Error(`Could not parse target name "${target}"`);
218 }
219 /* This ensures that the target has a scheme that is registered with the
220 * resolver */
221 const defaultSchemeMapResult = mapUriDefaultScheme(originalTargetUri);
222 if (defaultSchemeMapResult === null) {
223 throw new Error(
224 `Could not find a default scheme for target name "${target}"`
225 );
226 }
227
228 this.callRefTimer = setInterval(() => {}, MAX_TIMEOUT_TIME);
229 this.callRefTimer.unref?.();
230
231 if (this.options['grpc.enable_channelz'] === 0) {
232 this.channelzEnabled = false;
233 }
234
235 this.channelzTrace = new ChannelzTrace();
236 this.channelzRef = registerChannelzChannel(
237 target,
238 () => this.getChannelzInfo(),
239 this.channelzEnabled
240 );
241 if (this.channelzEnabled) {
242 this.channelzTrace.addTrace('CT_INFO', 'Channel created');
243 }
244
245 if (this.options['grpc.default_authority']) {
246 this.defaultAuthority = this.options['grpc.default_authority'] as string;
247 } else {
248 this.defaultAuthority = getDefaultAuthority(defaultSchemeMapResult);
249 }
250 const proxyMapResult = mapProxyName(defaultSchemeMapResult, options);
251 this.target = proxyMapResult.target;
252 this.options = Object.assign({}, this.options, proxyMapResult.extraOptions);
253
254 /* The global boolean parameter to getSubchannelPool has the inverse meaning to what
255 * the grpc.use_local_subchannel_pool channel option means. */
256 this.subchannelPool = getSubchannelPool(
257 (options['grpc.use_local_subchannel_pool'] ?? 0) === 0
258 );
259 this.retryBufferTracker = new MessageBufferTracker(
260 options['grpc.retry_buffer_size'] ?? DEFAULT_RETRY_BUFFER_SIZE_BYTES,
261 options['grpc.per_rpc_retry_buffer_size'] ??
262 DEFAULT_PER_RPC_RETRY_BUFFER_SIZE_BYTES
263 );
264 this.keepaliveTime = options['grpc.keepalive_time_ms'] ?? -1;
265 this.idleTimeoutMs = Math.max(
266 options['grpc.client_idle_timeout_ms'] ?? DEFAULT_IDLE_TIMEOUT_MS,
267 MIN_IDLE_TIMEOUT_MS
268 );
269 const channelControlHelper: ChannelControlHelper = {
270 createSubchannel: (
271 subchannelAddress: SubchannelAddress,
272 subchannelArgs: ChannelOptions
273 ) => {
274 const subchannel = this.subchannelPool.getOrCreateSubchannel(
275 this.target,
276 subchannelAddress,
277 Object.assign({}, this.options, subchannelArgs),
278 this.credentials
279 );
280 subchannel.throttleKeepalive(this.keepaliveTime);
281 if (this.channelzEnabled) {
282 this.channelzTrace.addTrace(
283 'CT_INFO',
284 'Created subchannel or used existing subchannel',
285 subchannel.getChannelzRef()
286 );
287 }
288 const wrappedSubchannel = new ChannelSubchannelWrapper(
289 subchannel,
290 this
291 );
292 this.wrappedSubchannels.add(wrappedSubchannel);
293 return wrappedSubchannel;
294 },
295 updateState: (connectivityState: ConnectivityState, picker: Picker) => {
296 this.currentPicker = picker;
297 const queueCopy = this.pickQueue.slice();
298 this.pickQueue = [];
299 this.callRefTimerUnref();
300 for (const call of queueCopy) {
301 call.doPick();
302 }
303 this.updateState(connectivityState);
304 },
305 requestReresolution: () => {
306 // This should never be called.
307 throw new Error(
308 'Resolving load balancer should never call requestReresolution'
309 );
310 },
311 addChannelzChild: (child: ChannelRef | SubchannelRef) => {
312 if (this.channelzEnabled) {
313 this.childrenTracker.refChild(child);
314 }
315 },
316 removeChannelzChild: (child: ChannelRef | SubchannelRef) => {
317 if (this.channelzEnabled) {
318 this.childrenTracker.unrefChild(child);
319 }
320 },
321 };
322 this.resolvingLoadBalancer = new ResolvingLoadBalancer(
323 this.target,
324 channelControlHelper,
325 options,
326 (serviceConfig, configSelector) => {
327 if (serviceConfig.retryThrottling) {
328 RETRY_THROTTLER_MAP.set(
329 this.getTarget(),
330 new RetryThrottler(
331 serviceConfig.retryThrottling.maxTokens,
332 serviceConfig.retryThrottling.tokenRatio,
333 RETRY_THROTTLER_MAP.get(this.getTarget())
334 )
335 );
336 } else {
337 RETRY_THROTTLER_MAP.delete(this.getTarget());
338 }
339 if (this.channelzEnabled) {
340 this.channelzTrace.addTrace(
341 'CT_INFO',
342 'Address resolution succeeded'
343 );
344 }
345 this.configSelector = configSelector;
346 this.currentResolutionError = null;
347 /* We process the queue asynchronously to ensure that the corresponding
348 * load balancer update has completed. */
349 process.nextTick(() => {
350 const localQueue = this.configSelectionQueue;
351 this.configSelectionQueue = [];
352 this.callRefTimerUnref();
353 for (const call of localQueue) {
354 call.getConfig();
355 }
356 this.configSelectionQueue = [];
357 });
358 },
359 status => {
360 if (this.channelzEnabled) {
361 this.channelzTrace.addTrace(
362 'CT_WARNING',
363 'Address resolution failed with code ' +
364 status.code +
365 ' and details "' +
366 status.details +
367 '"'
368 );
369 }
370 if (this.configSelectionQueue.length > 0) {
371 this.trace(
372 'Name resolution failed with calls queued for config selection'
373 );
374 }
375 if (this.configSelector === null) {
376 this.currentResolutionError = {
377 ...restrictControlPlaneStatusCode(status.code, status.details),
378 metadata: status.metadata,
379 };
380 }
381 const localQueue = this.configSelectionQueue;
382 this.configSelectionQueue = [];
383 this.callRefTimerUnref();
384 for (const call of localQueue) {
385 call.reportResolverError(status);
386 }
387 }
388 );
389 this.filterStackFactory = new FilterStackFactory([
390 new MaxMessageSizeFilterFactory(this.options),
391 new CompressionFilterFactory(this, this.options),
392 ]);
393 this.trace(
394 'Channel constructed with options ' +
395 JSON.stringify(options, undefined, 2)
396 );
397 const error = new Error();
398 trace(
399 LogVerbosity.DEBUG,
400 'channel_stacktrace',
401 '(' +
402 this.channelzRef.id +
403 ') ' +
404 'Channel constructed \n' +
405 error.stack?.substring(error.stack.indexOf('\n') + 1)
406 );
407 }
408
409 private getChannelzInfo(): ChannelInfo {
410 return {
411 target: this.originalTarget,
412 state: this.connectivityState,
413 trace: this.channelzTrace,
414 callTracker: this.callTracker,
415 children: this.childrenTracker.getChildLists(),
416 };
417 }
418
419 private trace(text: string, verbosityOverride?: LogVerbosity) {
420 trace(
421 verbosityOverride ?? LogVerbosity.DEBUG,
422 'channel',
423 '(' + this.channelzRef.id + ') ' + uriToString(this.target) + ' ' + text
424 );
425 }
426
427 private callRefTimerRef() {
428 // If the hasRef function does not exist, always run the code
429 if (!this.callRefTimer.hasRef?.()) {
430 this.trace(
431 'callRefTimer.ref | configSelectionQueue.length=' +
432 this.configSelectionQueue.length +
433 ' pickQueue.length=' +
434 this.pickQueue.length
435 );
436 this.callRefTimer.ref?.();
437 }
438 }
439
440 private callRefTimerUnref() {
441 // If the hasRef function does not exist, always run the code
442 if (!this.callRefTimer.hasRef || this.callRefTimer.hasRef()) {
443 this.trace(
444 'callRefTimer.unref | configSelectionQueue.length=' +
445 this.configSelectionQueue.length +
446 ' pickQueue.length=' +
447 this.pickQueue.length
448 );
449 this.callRefTimer.unref?.();
450 }
451 }
452
453 private removeConnectivityStateWatcher(
454 watcherObject: ConnectivityStateWatcher
455 ) {
456 const watcherIndex = this.connectivityStateWatchers.findIndex(
457 value => value === watcherObject
458 );
459 if (watcherIndex >= 0) {
460 this.connectivityStateWatchers.splice(watcherIndex, 1);
461 }
462 }
463
464 private updateState(newState: ConnectivityState): void {
465 trace(
466 LogVerbosity.DEBUG,
467 'connectivity_state',
468 '(' +
469 this.channelzRef.id +
470 ') ' +
471 uriToString(this.target) +
472 ' ' +
473 ConnectivityState[this.connectivityState] +
474 ' -> ' +
475 ConnectivityState[newState]
476 );
477 if (this.channelzEnabled) {
478 this.channelzTrace.addTrace(
479 'CT_INFO',
480 'Connectivity state change to ' + ConnectivityState[newState]
481 );
482 }
483 this.connectivityState = newState;
484 const watchersCopy = this.connectivityStateWatchers.slice();
485 for (const watcherObject of watchersCopy) {
486 if (newState !== watcherObject.currentState) {
487 if (watcherObject.timer) {
488 clearTimeout(watcherObject.timer);
489 }
490 this.removeConnectivityStateWatcher(watcherObject);
491 watcherObject.callback();
492 }
493 }
494 if (newState !== ConnectivityState.TRANSIENT_FAILURE) {
495 this.currentResolutionError = null;
496 }
497 }
498
499 throttleKeepalive(newKeepaliveTime: number) {
500 if (newKeepaliveTime > this.keepaliveTime) {
501 this.keepaliveTime = newKeepaliveTime;
502 for (const wrappedSubchannel of this.wrappedSubchannels) {
503 wrappedSubchannel.throttleKeepalive(newKeepaliveTime);
504 }
505 }
506 }
507
508 removeWrappedSubchannel(wrappedSubchannel: ChannelSubchannelWrapper) {
509 this.wrappedSubchannels.delete(wrappedSubchannel);
510 }
511
512 doPick(metadata: Metadata, extraPickInfo: { [key: string]: string }) {
513 return this.currentPicker.pick({
514 metadata: metadata,
515 extraPickInfo: extraPickInfo,
516 });
517 }
518
519 queueCallForPick(call: LoadBalancingCall) {
520 this.pickQueue.push(call);
521 this.callRefTimerRef();
522 }
523
524 getConfig(method: string, metadata: Metadata): GetConfigResult {
525 this.resolvingLoadBalancer.exitIdle();
526 if (this.configSelector) {
527 return {
528 type: 'SUCCESS',
529 config: this.configSelector(method, metadata),
530 };
531 } else {
532 if (this.currentResolutionError) {
533 return {
534 type: 'ERROR',
535 error: this.currentResolutionError,
536 };
537 } else {
538 return {
539 type: 'NONE',
540 };
541 }
542 }
543 }
544
545 queueCallForConfig(call: ResolvingCall) {
546 this.configSelectionQueue.push(call);
547 this.callRefTimerRef();
548 }
549
550 private enterIdle() {
551 this.resolvingLoadBalancer.destroy();
552 this.updateState(ConnectivityState.IDLE);
553 this.currentPicker = new QueuePicker(this.resolvingLoadBalancer);
554 }
555
556 private maybeStartIdleTimer() {
557 if (this.callCount === 0) {
558 this.idleTimer = setTimeout(() => {
559 this.trace(
560 'Idle timer triggered after ' +
561 this.idleTimeoutMs +
562 'ms of inactivity'
563 );
564 this.enterIdle();
565 }, this.idleTimeoutMs);
566 this.idleTimer.unref?.();
567 }
568 }
569
570 private onCallStart() {
571 if (this.channelzEnabled) {
572 this.callTracker.addCallStarted();
573 }
574 this.callCount += 1;
575 if (this.idleTimer) {
576 clearTimeout(this.idleTimer);
577 this.idleTimer = null;
578 }
579 }
580
581 private onCallEnd(status: StatusObject) {
582 if (this.channelzEnabled) {
583 if (status.code === Status.OK) {
584 this.callTracker.addCallSucceeded();
585 } else {
586 this.callTracker.addCallFailed();
587 }
588 }
589 this.callCount -= 1;
590 this.maybeStartIdleTimer();
591 }
592
593 createLoadBalancingCall(
594 callConfig: CallConfig,
595 method: string,
596 host: string,
597 credentials: CallCredentials,
598 deadline: Deadline
599 ): LoadBalancingCall {
600 const callNumber = getNextCallNumber();
601 this.trace(
602 'createLoadBalancingCall [' + callNumber + '] method="' + method + '"'
603 );
604 return new LoadBalancingCall(
605 this,
606 callConfig,
607 method,
608 host,
609 credentials,
610 deadline,
611 callNumber
612 );
613 }
614
615 createRetryingCall(
616 callConfig: CallConfig,
617 method: string,
618 host: string,
619 credentials: CallCredentials,
620 deadline: Deadline
621 ): RetryingCall {
622 const callNumber = getNextCallNumber();
623 this.trace(
624 'createRetryingCall [' + callNumber + '] method="' + method + '"'
625 );
626 return new RetryingCall(
627 this,
628 callConfig,
629 method,
630 host,
631 credentials,
632 deadline,
633 callNumber,
634 this.retryBufferTracker,
635 RETRY_THROTTLER_MAP.get(this.getTarget())
636 );
637 }
638
639 createInnerCall(
640 callConfig: CallConfig,
641 method: string,
642 host: string,
643 credentials: CallCredentials,
644 deadline: Deadline
645 ): Call {
646 // Create a RetryingCall if retries are enabled
647 if (this.options['grpc.enable_retries'] === 0) {
648 return this.createLoadBalancingCall(
649 callConfig,
650 method,
651 host,
652 credentials,
653 deadline
654 );
655 } else {
656 return this.createRetryingCall(
657 callConfig,
658 method,
659 host,
660 credentials,
661 deadline
662 );
663 }
664 }
665
666 createResolvingCall(
667 method: string,
668 deadline: Deadline,
669 host: string | null | undefined,
670 parentCall: ServerSurfaceCall | null,
671 propagateFlags: number | null | undefined
672 ): ResolvingCall {
673 const callNumber = getNextCallNumber();
674 this.trace(
675 'createResolvingCall [' +
676 callNumber +
677 '] method="' +
678 method +
679 '", deadline=' +
680 deadlineToString(deadline)
681 );
682 const finalOptions: CallStreamOptions = {
683 deadline: deadline,
684 flags: propagateFlags ?? Propagate.DEFAULTS,
685 host: host ?? this.defaultAuthority,
686 parentCall: parentCall,
687 };
688
689 const call = new ResolvingCall(
690 this,
691 method,
692 finalOptions,
693 this.filterStackFactory.clone(),
694 this.credentials._getCallCredentials(),
695 callNumber
696 );
697
698 this.onCallStart();
699 call.addStatusWatcher(status => {
700 this.onCallEnd(status);
701 });
702 return call;
703 }
704
705 close() {
706 this.resolvingLoadBalancer.destroy();
707 this.updateState(ConnectivityState.SHUTDOWN);
708 clearInterval(this.callRefTimer);
709 if (this.channelzEnabled) {
710 unregisterChannelzRef(this.channelzRef);
711 }
712
713 this.subchannelPool.unrefUnusedSubchannels();
714 }
715
716 getTarget() {
717 return uriToString(this.target);
718 }
719
720 getConnectivityState(tryToConnect: boolean) {
721 const connectivityState = this.connectivityState;
722 if (tryToConnect) {
723 this.resolvingLoadBalancer.exitIdle();
724 this.maybeStartIdleTimer();
725 }
726 return connectivityState;
727 }
728
729 watchConnectivityState(
730 currentState: ConnectivityState,
731 deadline: Date | number,
732 callback: (error?: Error) => void
733 ): void {
734 if (this.connectivityState === ConnectivityState.SHUTDOWN) {
735 throw new Error('Channel has been shut down');
736 }
737 let timer = null;
738 if (deadline !== Infinity) {
739 const deadlineDate: Date =
740 deadline instanceof Date ? deadline : new Date(deadline);
741 const now = new Date();
742 if (deadline === -Infinity || deadlineDate <= now) {
743 process.nextTick(
744 callback,
745 new Error('Deadline passed without connectivity state change')
746 );
747 return;
748 }
749 timer = setTimeout(() => {
750 this.removeConnectivityStateWatcher(watcherObject);
751 callback(
752 new Error('Deadline passed without connectivity state change')
753 );
754 }, deadlineDate.getTime() - now.getTime());
755 }
756 const watcherObject = {
757 currentState,
758 callback,
759 timer,
760 };
761 this.connectivityStateWatchers.push(watcherObject);
762 }
763
764 /**
765 * Get the channelz reference object for this channel. The returned value is
766 * garbage if channelz is disabled for this channel.
767 * @returns
768 */
769 getChannelzRef() {
770 return this.channelzRef;
771 }
772
773 createCall(
774 method: string,
775 deadline: Deadline,
776 host: string | null | undefined,
777 parentCall: ServerSurfaceCall | null,
778 propagateFlags: number | null | undefined
779 ): Call {
780 if (typeof method !== 'string') {
781 throw new TypeError('Channel#createCall: method must be a string');
782 }
783 if (!(typeof deadline === 'number' || deadline instanceof Date)) {
784 throw new TypeError(
785 'Channel#createCall: deadline must be a number or Date'
786 );
787 }
788 if (this.connectivityState === ConnectivityState.SHUTDOWN) {
789 throw new Error('Channel has been shut down');
790 }
791 return this.createResolvingCall(
792 method,
793 deadline,
794 host,
795 parentCall,
796 propagateFlags
797 );
798 }
799}