"use strict"; /* * Copyright 2019 gRPC authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ Object.defineProperty(exports, "__esModule", { value: true }); exports.InternalChannel = exports.SUBCHANNEL_ARGS_EXCLUDE_KEY_PREFIX = void 0; const channel_credentials_1 = require("./channel-credentials"); const resolving_load_balancer_1 = require("./resolving-load-balancer"); const subchannel_pool_1 = require("./subchannel-pool"); const picker_1 = require("./picker"); const metadata_1 = require("./metadata"); const constants_1 = require("./constants"); const filter_stack_1 = require("./filter-stack"); const compression_filter_1 = require("./compression-filter"); const resolver_1 = require("./resolver"); const logging_1 = require("./logging"); const http_proxy_1 = require("./http_proxy"); const uri_parser_1 = require("./uri-parser"); const connectivity_state_1 = require("./connectivity-state"); const channelz_1 = require("./channelz"); const load_balancing_call_1 = require("./load-balancing-call"); const deadline_1 = require("./deadline"); const resolving_call_1 = require("./resolving-call"); const call_number_1 = require("./call-number"); const control_plane_status_1 = require("./control-plane-status"); const retrying_call_1 = require("./retrying-call"); const subchannel_interface_1 = require("./subchannel-interface"); /** * See https://nodejs.org/api/timers.html#timers_setinterval_callback_delay_args */ const MAX_TIMEOUT_TIME = 2147483647; const MIN_IDLE_TIMEOUT_MS = 1000; // 30 minutes const DEFAULT_IDLE_TIMEOUT_MS = 30 * 60 * 1000; const RETRY_THROTTLER_MAP = new Map(); const DEFAULT_RETRY_BUFFER_SIZE_BYTES = 1 << 24; // 16 MB const DEFAULT_PER_RPC_RETRY_BUFFER_SIZE_BYTES = 1 << 20; // 1 MB class ChannelSubchannelWrapper extends subchannel_interface_1.BaseSubchannelWrapper { constructor(childSubchannel, channel) { super(childSubchannel); this.channel = channel; this.refCount = 0; this.subchannelStateListener = (subchannel, previousState, newState, keepaliveTime) => { channel.throttleKeepalive(keepaliveTime); }; } ref() { if (this.refCount === 0) { this.child.addConnectivityStateListener(this.subchannelStateListener); this.channel.addWrappedSubchannel(this); } this.child.ref(); this.refCount += 1; } unref() { this.child.unref(); this.refCount -= 1; if (this.refCount <= 0) { this.child.removeConnectivityStateListener(this.subchannelStateListener); this.channel.removeWrappedSubchannel(this); } } } class ShutdownPicker { pick(pickArgs) { return { pickResultType: picker_1.PickResultType.DROP, status: { code: constants_1.Status.UNAVAILABLE, details: 'Channel closed before call started', metadata: new metadata_1.Metadata() }, subchannel: null, onCallStarted: null, onCallEnded: null }; } } exports.SUBCHANNEL_ARGS_EXCLUDE_KEY_PREFIX = 'grpc.internal.no_subchannel'; class ChannelzInfoTracker { constructor(target) { this.target = target; this.trace = new channelz_1.ChannelzTrace(); this.callTracker = new channelz_1.ChannelzCallTracker(); this.childrenTracker = new channelz_1.ChannelzChildrenTracker(); this.state = connectivity_state_1.ConnectivityState.IDLE; } getChannelzInfoCallback() { return () => { return { target: this.target, state: this.state, trace: this.trace, callTracker: this.callTracker, children: this.childrenTracker.getChildLists() }; }; } } class InternalChannel { constructor(target, credentials, options) { var _a, _b, _c, _d, _e, _f; this.credentials = credentials; this.options = options; this.connectivityState = connectivity_state_1.ConnectivityState.IDLE; this.currentPicker = new picker_1.UnavailablePicker(); /** * Calls queued up to get a call config. Should only be populated before the * first time the resolver returns a result, which includes the ConfigSelector. */ this.configSelectionQueue = []; this.pickQueue = []; this.connectivityStateWatchers = []; /** * This timer does not do anything on its own. Its purpose is to hold the * event loop open while there are any pending calls for the channel that * have not yet been assigned to specific subchannels. In other words, * the invariant is that callRefTimer is reffed if and only if pickQueue * is non-empty. In addition, the timer is null while the state is IDLE or * SHUTDOWN and there are no pending calls. */ this.callRefTimer = null; this.configSelector = null; /** * This is the error from the name resolver if it failed most recently. It * is only used to end calls that start while there is no config selector * and the name resolver is in backoff, so it should be nulled if * configSelector becomes set or the channel state becomes anything other * than TRANSIENT_FAILURE. */ this.currentResolutionError = null; this.wrappedSubchannels = new Set(); this.callCount = 0; this.idleTimer = null; // Channelz info this.channelzEnabled = true; /** * Randomly generated ID to be passed to the config selector, for use by * ring_hash in xDS. An integer distributed approximately uniformly between * 0 and MAX_SAFE_INTEGER. */ this.randomChannelId = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER); if (typeof target !== 'string') { throw new TypeError('Channel target must be a string'); } if (!(credentials instanceof channel_credentials_1.ChannelCredentials)) { throw new TypeError('Channel credentials must be a ChannelCredentials object'); } if (options) { if (typeof options !== 'object') { throw new TypeError('Channel options must be an object'); } } this.channelzInfoTracker = new ChannelzInfoTracker(target); const originalTargetUri = (0, uri_parser_1.parseUri)(target); if (originalTargetUri === null) { throw new Error(`Could not parse target name "${target}"`); } /* This ensures that the target has a scheme that is registered with the * resolver */ const defaultSchemeMapResult = (0, resolver_1.mapUriDefaultScheme)(originalTargetUri); if (defaultSchemeMapResult === null) { throw new Error(`Could not find a default scheme for target name "${target}"`); } if (this.options['grpc.enable_channelz'] === 0) { this.channelzEnabled = false; } this.channelzRef = (0, channelz_1.registerChannelzChannel)(target, this.channelzInfoTracker.getChannelzInfoCallback(), this.channelzEnabled); if (this.channelzEnabled) { this.channelzInfoTracker.trace.addTrace('CT_INFO', 'Channel created'); } if (this.options['grpc.default_authority']) { this.defaultAuthority = this.options['grpc.default_authority']; } else { this.defaultAuthority = (0, resolver_1.getDefaultAuthority)(defaultSchemeMapResult); } const proxyMapResult = (0, http_proxy_1.mapProxyName)(defaultSchemeMapResult, options); this.target = proxyMapResult.target; this.options = Object.assign({}, this.options, proxyMapResult.extraOptions); /* The global boolean parameter to getSubchannelPool has the inverse meaning to what * the grpc.use_local_subchannel_pool channel option means. */ this.subchannelPool = (0, subchannel_pool_1.getSubchannelPool)(((_a = this.options['grpc.use_local_subchannel_pool']) !== null && _a !== void 0 ? _a : 0) === 0); this.retryBufferTracker = new retrying_call_1.MessageBufferTracker((_b = this.options['grpc.retry_buffer_size']) !== null && _b !== void 0 ? _b : DEFAULT_RETRY_BUFFER_SIZE_BYTES, (_c = this.options['grpc.per_rpc_retry_buffer_size']) !== null && _c !== void 0 ? _c : DEFAULT_PER_RPC_RETRY_BUFFER_SIZE_BYTES); this.keepaliveTime = (_d = this.options['grpc.keepalive_time_ms']) !== null && _d !== void 0 ? _d : -1; this.idleTimeoutMs = Math.max((_e = this.options['grpc.client_idle_timeout_ms']) !== null && _e !== void 0 ? _e : DEFAULT_IDLE_TIMEOUT_MS, MIN_IDLE_TIMEOUT_MS); const channelControlHelper = { createSubchannel: (subchannelAddress, subchannelArgs) => { const finalSubchannelArgs = {}; for (const [key, value] of Object.entries(subchannelArgs)) { if (!key.startsWith(exports.SUBCHANNEL_ARGS_EXCLUDE_KEY_PREFIX)) { finalSubchannelArgs[key] = value; } } const subchannel = this.subchannelPool.getOrCreateSubchannel(this.target, subchannelAddress, finalSubchannelArgs, this.credentials); subchannel.throttleKeepalive(this.keepaliveTime); if (this.channelzEnabled) { this.channelzInfoTracker.trace.addTrace('CT_INFO', 'Created subchannel or used existing subchannel', subchannel.getChannelzRef()); } const wrappedSubchannel = new ChannelSubchannelWrapper(subchannel, this); return wrappedSubchannel; }, updateState: (connectivityState, picker) => { this.currentPicker = picker; const queueCopy = this.pickQueue.slice(); this.pickQueue = []; if (queueCopy.length > 0) { this.callRefTimerUnref(); } for (const call of queueCopy) { call.doPick(); } this.updateState(connectivityState); }, requestReresolution: () => { // This should never be called. throw new Error('Resolving load balancer should never call requestReresolution'); }, addChannelzChild: (child) => { if (this.channelzEnabled) { this.channelzInfoTracker.childrenTracker.refChild(child); } }, removeChannelzChild: (child) => { if (this.channelzEnabled) { this.channelzInfoTracker.childrenTracker.unrefChild(child); } }, }; this.resolvingLoadBalancer = new resolving_load_balancer_1.ResolvingLoadBalancer(this.target, channelControlHelper, this.options, (serviceConfig, configSelector) => { var _a; if (serviceConfig.retryThrottling) { RETRY_THROTTLER_MAP.set(this.getTarget(), new retrying_call_1.RetryThrottler(serviceConfig.retryThrottling.maxTokens, serviceConfig.retryThrottling.tokenRatio, RETRY_THROTTLER_MAP.get(this.getTarget()))); } else { RETRY_THROTTLER_MAP.delete(this.getTarget()); } if (this.channelzEnabled) { this.channelzInfoTracker.trace.addTrace('CT_INFO', 'Address resolution succeeded'); } (_a = this.configSelector) === null || _a === void 0 ? void 0 : _a.unref(); this.configSelector = configSelector; this.currentResolutionError = null; /* We process the queue asynchronously to ensure that the corresponding * load balancer update has completed. */ process.nextTick(() => { const localQueue = this.configSelectionQueue; this.configSelectionQueue = []; if (localQueue.length > 0) { this.callRefTimerUnref(); } for (const call of localQueue) { call.getConfig(); } }); }, status => { if (this.channelzEnabled) { this.channelzInfoTracker.trace.addTrace('CT_WARNING', 'Address resolution failed with code ' + status.code + ' and details "' + status.details + '"'); } if (this.configSelectionQueue.length > 0) { this.trace('Name resolution failed with calls queued for config selection'); } if (this.configSelector === null) { this.currentResolutionError = Object.assign(Object.assign({}, (0, control_plane_status_1.restrictControlPlaneStatusCode)(status.code, status.details)), { metadata: status.metadata }); } const localQueue = this.configSelectionQueue; this.configSelectionQueue = []; if (localQueue.length > 0) { this.callRefTimerUnref(); } for (const call of localQueue) { call.reportResolverError(status); } }); this.filterStackFactory = new filter_stack_1.FilterStackFactory([ new compression_filter_1.CompressionFilterFactory(this, this.options), ]); this.trace('Channel constructed with options ' + JSON.stringify(options, undefined, 2)); const error = new Error(); if ((0, logging_1.isTracerEnabled)('channel_stacktrace')) { (0, logging_1.trace)(constants_1.LogVerbosity.DEBUG, 'channel_stacktrace', '(' + this.channelzRef.id + ') ' + 'Channel constructed \n' + ((_f = error.stack) === null || _f === void 0 ? void 0 : _f.substring(error.stack.indexOf('\n') + 1))); } this.lastActivityTimestamp = new Date(); } trace(text, verbosityOverride) { (0, logging_1.trace)(verbosityOverride !== null && verbosityOverride !== void 0 ? verbosityOverride : constants_1.LogVerbosity.DEBUG, 'channel', '(' + this.channelzRef.id + ') ' + (0, uri_parser_1.uriToString)(this.target) + ' ' + text); } callRefTimerRef() { var _a, _b, _c, _d; if (!this.callRefTimer) { this.callRefTimer = setInterval(() => { }, MAX_TIMEOUT_TIME); } // If the hasRef function does not exist, always run the code if (!((_b = (_a = this.callRefTimer).hasRef) === null || _b === void 0 ? void 0 : _b.call(_a))) { this.trace('callRefTimer.ref | configSelectionQueue.length=' + this.configSelectionQueue.length + ' pickQueue.length=' + this.pickQueue.length); (_d = (_c = this.callRefTimer).ref) === null || _d === void 0 ? void 0 : _d.call(_c); } } callRefTimerUnref() { var _a, _b, _c; // If the timer or the hasRef function does not exist, always run the code if (!((_a = this.callRefTimer) === null || _a === void 0 ? void 0 : _a.hasRef) || this.callRefTimer.hasRef()) { this.trace('callRefTimer.unref | configSelectionQueue.length=' + this.configSelectionQueue.length + ' pickQueue.length=' + this.pickQueue.length); (_c = (_b = this.callRefTimer) === null || _b === void 0 ? void 0 : _b.unref) === null || _c === void 0 ? void 0 : _c.call(_b); } } removeConnectivityStateWatcher(watcherObject) { const watcherIndex = this.connectivityStateWatchers.findIndex(value => value === watcherObject); if (watcherIndex >= 0) { this.connectivityStateWatchers.splice(watcherIndex, 1); } } updateState(newState) { (0, logging_1.trace)(constants_1.LogVerbosity.DEBUG, 'connectivity_state', '(' + this.channelzRef.id + ') ' + (0, uri_parser_1.uriToString)(this.target) + ' ' + connectivity_state_1.ConnectivityState[this.connectivityState] + ' -> ' + connectivity_state_1.ConnectivityState[newState]); if (this.channelzEnabled) { this.channelzInfoTracker.trace.addTrace('CT_INFO', 'Connectivity state change to ' + connectivity_state_1.ConnectivityState[newState]); } this.connectivityState = newState; this.channelzInfoTracker.state = newState; const watchersCopy = this.connectivityStateWatchers.slice(); for (const watcherObject of watchersCopy) { if (newState !== watcherObject.currentState) { if (watcherObject.timer) { clearTimeout(watcherObject.timer); } this.removeConnectivityStateWatcher(watcherObject); watcherObject.callback(); } } if (newState !== connectivity_state_1.ConnectivityState.TRANSIENT_FAILURE) { this.currentResolutionError = null; } } throttleKeepalive(newKeepaliveTime) { if (newKeepaliveTime > this.keepaliveTime) { this.keepaliveTime = newKeepaliveTime; for (const wrappedSubchannel of this.wrappedSubchannels) { wrappedSubchannel.throttleKeepalive(newKeepaliveTime); } } } addWrappedSubchannel(wrappedSubchannel) { this.wrappedSubchannels.add(wrappedSubchannel); } removeWrappedSubchannel(wrappedSubchannel) { this.wrappedSubchannels.delete(wrappedSubchannel); } doPick(metadata, extraPickInfo) { return this.currentPicker.pick({ metadata: metadata, extraPickInfo: extraPickInfo, }); } queueCallForPick(call) { this.pickQueue.push(call); this.callRefTimerRef(); } getConfig(method, metadata) { if (this.connectivityState !== connectivity_state_1.ConnectivityState.SHUTDOWN) { this.resolvingLoadBalancer.exitIdle(); } if (this.configSelector) { return { type: 'SUCCESS', config: this.configSelector.invoke(method, metadata, this.randomChannelId), }; } else { if (this.currentResolutionError) { return { type: 'ERROR', error: this.currentResolutionError, }; } else { return { type: 'NONE', }; } } } queueCallForConfig(call) { this.configSelectionQueue.push(call); this.callRefTimerRef(); } enterIdle() { this.resolvingLoadBalancer.destroy(); this.updateState(connectivity_state_1.ConnectivityState.IDLE); this.currentPicker = new picker_1.QueuePicker(this.resolvingLoadBalancer); if (this.idleTimer) { clearTimeout(this.idleTimer); this.idleTimer = null; } if (this.callRefTimer) { clearInterval(this.callRefTimer); this.callRefTimer = null; } } startIdleTimeout(timeoutMs) { var _a, _b; 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(); } 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); (_b = (_a = this.idleTimer).unref) === null || _b === void 0 ? void 0 : _b.call(_a); } maybeStartIdleTimer() { if (this.connectivityState !== connectivity_state_1.ConnectivityState.SHUTDOWN && !this.idleTimer) { this.startIdleTimeout(this.idleTimeoutMs); } } onCallStart() { if (this.channelzEnabled) { this.channelzInfoTracker.callTracker.addCallStarted(); } this.callCount += 1; } onCallEnd(status) { if (this.channelzEnabled) { if (status.code === constants_1.Status.OK) { this.channelzInfoTracker.callTracker.addCallSucceeded(); } else { this.channelzInfoTracker.callTracker.addCallFailed(); } } this.callCount -= 1; this.lastActivityTimestamp = new Date(); this.maybeStartIdleTimer(); } createLoadBalancingCall(callConfig, method, host, credentials, deadline) { const callNumber = (0, call_number_1.getNextCallNumber)(); this.trace('createLoadBalancingCall [' + callNumber + '] method="' + method + '"'); return new load_balancing_call_1.LoadBalancingCall(this, callConfig, method, host, credentials, deadline, callNumber); } createRetryingCall(callConfig, method, host, credentials, deadline) { const callNumber = (0, call_number_1.getNextCallNumber)(); this.trace('createRetryingCall [' + callNumber + '] method="' + method + '"'); return new retrying_call_1.RetryingCall(this, callConfig, method, host, credentials, deadline, callNumber, this.retryBufferTracker, RETRY_THROTTLER_MAP.get(this.getTarget())); } createResolvingCall(method, deadline, host, parentCall, propagateFlags) { const callNumber = (0, call_number_1.getNextCallNumber)(); this.trace('createResolvingCall [' + callNumber + '] method="' + method + '", deadline=' + (0, deadline_1.deadlineToString)(deadline)); const finalOptions = { deadline: deadline, flags: propagateFlags !== null && propagateFlags !== void 0 ? propagateFlags : constants_1.Propagate.DEFAULTS, host: host !== null && host !== void 0 ? host : this.defaultAuthority, parentCall: parentCall, }; const call = new resolving_call_1.ResolvingCall(this, method, finalOptions, this.filterStackFactory.clone(), callNumber); this.onCallStart(); call.addStatusWatcher(status => { this.onCallEnd(status); }); return call; } close() { var _a; this.resolvingLoadBalancer.destroy(); this.updateState(connectivity_state_1.ConnectivityState.SHUTDOWN); this.currentPicker = new ShutdownPicker(); for (const call of this.configSelectionQueue) { call.cancelWithStatus(constants_1.Status.UNAVAILABLE, 'Channel closed before call started'); } this.configSelectionQueue = []; for (const call of this.pickQueue) { call.cancelWithStatus(constants_1.Status.UNAVAILABLE, 'Channel closed before call started'); } this.pickQueue = []; if (this.callRefTimer) { clearInterval(this.callRefTimer); } if (this.idleTimer) { clearTimeout(this.idleTimer); } if (this.channelzEnabled) { (0, channelz_1.unregisterChannelzRef)(this.channelzRef); } this.subchannelPool.unrefUnusedSubchannels(); (_a = this.configSelector) === null || _a === void 0 ? void 0 : _a.unref(); this.configSelector = null; } getTarget() { return (0, uri_parser_1.uriToString)(this.target); } getConnectivityState(tryToConnect) { const connectivityState = this.connectivityState; if (tryToConnect) { this.resolvingLoadBalancer.exitIdle(); this.lastActivityTimestamp = new Date(); this.maybeStartIdleTimer(); } return connectivityState; } watchConnectivityState(currentState, deadline, callback) { if (this.connectivityState === connectivity_state_1.ConnectivityState.SHUTDOWN) { throw new Error('Channel has been shut down'); } let timer = null; if (deadline !== Infinity) { const deadlineDate = deadline instanceof Date ? deadline : new Date(deadline); const now = new Date(); if (deadline === -Infinity || deadlineDate <= now) { process.nextTick(callback, new Error('Deadline passed without connectivity state change')); return; } timer = setTimeout(() => { this.removeConnectivityStateWatcher(watcherObject); callback(new Error('Deadline passed without connectivity state change')); }, deadlineDate.getTime() - now.getTime()); } const watcherObject = { currentState, callback, timer, }; this.connectivityStateWatchers.push(watcherObject); } /** * Get the channelz reference object for this channel. The returned value is * garbage if channelz is disabled for this channel. * @returns */ getChannelzRef() { return this.channelzRef; } createCall(method, deadline, host, parentCall, propagateFlags) { if (typeof method !== 'string') { throw new TypeError('Channel#createCall: method must be a string'); } if (!(typeof deadline === 'number' || deadline instanceof Date)) { throw new TypeError('Channel#createCall: deadline must be a number or Date'); } if (this.connectivityState === connectivity_state_1.ConnectivityState.SHUTDOWN) { throw new Error('Channel has been shut down'); } return this.createResolvingCall(method, deadline, host, parentCall, propagateFlags); } getOptions() { return this.options; } } exports.InternalChannel = InternalChannel; //# sourceMappingURL=internal-channel.js.map