UNPKG

grpc-gcp

Version:

Extension for supporting Google Cloud Platform specific features for gRPC.

329 lines 14.4 kB
"use strict"; /** * @license * Copyright 2018 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. * */ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.getGcpChannelFactoryClass = void 0; const util_1 = require("util"); const channel_ref_1 = require("./channel_ref"); const grpc_js_1 = require("@grpc/grpc-js"); const CLIENT_CHANNEL_ID = 'grpc_gcp.client_channel.id'; function getGcpChannelFactoryClass(grpc) { /** * A channel management factory that implements grpc.Channel APIs. */ return class GcpChannelFactory { /** * @param address The address of the server to connect to. * @param credentials Channel credentials to use when connecting * @param options A map of channel options. */ constructor(address, credentials, // eslint-disable-next-line @typescript-eslint/no-explicit-any options) { this.methodToAffinity = {}; this.affinityKeyToChannelRef = {}; this.channelRefs = []; if (!options) { options = {}; } if (typeof options !== 'object') { throw new TypeError('Channel options must be an object with string keys and integer or string values'); } this.minSize = 1; this.maxSize = 10; this.maxConcurrentStreamsLowWatermark = 100; this.debugHeaderIntervalSecs = 0; const gcpApiConfig = options.gcpApiConfig; if (gcpApiConfig) { if (gcpApiConfig.channelPool) { const channelPool = gcpApiConfig.channelPool; if (channelPool.minSize) this.minSize = channelPool.minSize; if (channelPool.maxSize) this.maxSize = channelPool.maxSize; if (channelPool.maxConcurrentStreamsLowWatermark) { this.maxConcurrentStreamsLowWatermark = channelPool.maxConcurrentStreamsLowWatermark; } if (this.maxSize < this.minSize) { throw new Error('Invalid channelPool config: minSize must <= maxSize'); } this.debugHeaderIntervalSecs = channelPool.debugHeaderIntervalSecs || 0; } this.initMethodToAffinityMap(gcpApiConfig); } delete options.gcpApiConfig; this.options = options; this.target = address; this.credentials = credentials; // Create initial channels for (let i = 0; i < this.minSize; i++) { this.addChannel(); } } getChannelzRef() { return this.channelRefs[0].getChannel().getChannelzRef(); } initMethodToAffinityMap(gcpApiConfig) { const methodList = gcpApiConfig.method; if (methodList) { for (let i = 0; i < methodList.length; i++) { const method = methodList[i]; const nameList = method.name; if (nameList) { for (let j = 0; j < nameList.length; j++) { const methodName = nameList[j]; if (method.affinity) { this.methodToAffinity[methodName] = method.affinity; } } } } } } /** * Picks a grpc channel from the pool and wraps it with ChannelRef. * @param affinityKey Affinity key to get the bound channel. * @return Wrapper containing the grpc channel. */ getChannelRef(affinityKey) { if (affinityKey && this.affinityKeyToChannelRef[affinityKey]) { // Chose an bound channel if affinityKey is specified. return this.affinityKeyToChannelRef[affinityKey]; } // Sort channel refs by active streams count. this.channelRefs.sort((ref1, ref2) => { return ref1.getActiveStreamsCount() - ref2.getActiveStreamsCount(); }); const size = this.channelRefs.length; // Chose the channelRef that has the least busy channel. if (size > 0 && this.channelRefs[0].getActiveStreamsCount() < this.maxConcurrentStreamsLowWatermark) { return this.channelRefs[0]; } // If all existing channels are busy, and channel pool still has capacity, // create a new channel in the pool. if (size < this.maxSize) { return this.addChannel(); } else { return this.channelRefs[0]; } } /** * Create a new channel and add it to the pool. * @private */ addChannel() { const size = this.channelRefs.length; const channelOptions = Object.assign({ [CLIENT_CHANNEL_ID]: size }, this.options); const grpcChannel = new grpc.Channel(this.target, this.credentials, channelOptions); const channelRef = new channel_ref_1.ChannelRef(grpcChannel, size); this.channelRefs.push(channelRef); if (this.debugHeaderIntervalSecs) { this.setupDebugHeadersOnChannelTransition(channelRef); } return channelRef; } setupDebugHeadersOnChannelTransition(channel) { const self = this; if (channel.isClosed()) { return; } let currentState = channel.getChannel().getConnectivityState(false); if (currentState == grpc_js_1.connectivityState.SHUTDOWN) { return; } channel.getChannel().watchConnectivityState(currentState, Infinity, (e) => { channel.forceDebugHeadersOnNextRequest(); self.setupDebugHeadersOnChannelTransition(channel); }); } /** * Get AffinityConfig associated with a certain method. * @param methodName Method name of the request. */ getAffinityConfig(methodName) { return this.methodToAffinity[methodName]; } shouldRequestDebugHeaders(lastRequested) { if (this.debugHeaderIntervalSecs < 0) return true; else if (this.debugHeaderIntervalSecs == 0) return false; else if (!lastRequested) return true; return new Date().getTime() - lastRequested.getTime() > this.debugHeaderIntervalSecs * 1000; } /** * Bind channel with affinity key. * @param channelRef ChannelRef instance that contains the grpc channel. * @param affinityKey The affinity key used for binding the channel. */ bind(channelRef, affinityKey) { if (!affinityKey || !channelRef) return; const existingChannelRef = this.affinityKeyToChannelRef[affinityKey]; if (!existingChannelRef) { this.affinityKeyToChannelRef[affinityKey] = channelRef; } this.affinityKeyToChannelRef[affinityKey].affinityCountIncr(); } /** * Unbind channel with affinity key. * @param boundKey Affinity key bound to a channel. */ unbind(boundKey) { if (!boundKey) return; const boundChannelRef = this.affinityKeyToChannelRef[boundKey]; if (boundChannelRef) { boundChannelRef.affinityCountDecr(); if (boundChannelRef.getAffinityCount() <= 0) { delete this.affinityKeyToChannelRef[boundKey]; } } } /** * Close all channels in the channel pool. */ close() { this.channelRefs.forEach(ref => { ref.close(); }); } getTarget() { return this.target; } /** * Get the current connectivity state of the channel pool. * @param tryToConnect If true, the channel will start connecting if it is * idle. Otherwise, idle channels will only start connecting when a * call starts. * @return connectivity state of channel pool. */ getConnectivityState(tryToConnect) { let ready = 0; let idle = 0; let connecting = 0; let transientFailure = 0; let shutdown = 0; for (let i = 0; i < this.channelRefs.length; i++) { const grpcChannel = this.channelRefs[i].getChannel(); const state = grpcChannel.getConnectivityState(tryToConnect); switch (state) { case grpc.connectivityState.READY: ready++; break; case grpc.connectivityState.SHUTDOWN: shutdown++; break; case grpc.connectivityState.TRANSIENT_FAILURE: transientFailure++; break; case grpc.connectivityState.CONNECTING: connecting++; break; case grpc.connectivityState.IDLE: idle++; break; default: break; } } if (ready > 0) { return grpc.connectivityState.READY; } else if (connecting > 0) { return grpc.connectivityState.CONNECTING; } else if (transientFailure > 0) { return grpc.connectivityState.TRANSIENT_FAILURE; } else if (idle > 0) { return grpc.connectivityState.IDLE; } else if (shutdown > 0) { return grpc.connectivityState.SHUTDOWN; } throw new Error('Cannot get connectivity state because no channel provides valid state.'); } /** * Watch for connectivity state changes. * @param currentState The state to watch for transitions from. This should * always be populated by calling getConnectivityState immediately * before. * @param deadline A deadline for waiting for a state change * @param callback Called with no error when the state changes, or with an * error if the deadline passes without a state change */ watchConnectivityState(currentState, deadline, callback) { if (!this.channelRefs.length) { callback(new Error('Cannot watch connectivity state because there are no channels.')); return; } const connectivityState = this.getConnectivityState(false); if (connectivityState !== currentState) { setImmediate(() => callback()); return; } const watchState = (channelRef) => __awaiter(this, void 0, void 0, function* () { const channel = channelRef.getChannel(); const startingState = channel.getConnectivityState(false); yield (0, util_1.promisify)(channel.watchConnectivityState).call(channel, startingState, deadline); const state = this.getConnectivityState(false); if (state === currentState) { return watchState(channelRef); } }); const watchers = this.channelRefs.map(watchState); Promise.race(watchers).then(() => callback(), callback); } /** * Create a call object. This function will not be called when using * grpc.Client class. But since it's a public function of grpc.Channel, * It needs to be implemented for potential use cases. * @param method The full method string to request. * @param deadline The call deadline. * @param host A host string override for making the request. * @param parentCall A server call to propagate some information from. * @param propagateFlags A bitwise combination of elements of * {@link grpc.propagate} that indicates what information to propagate * from parentCall. * @return a grpc call object. */ createCall(method, deadline, host, // eslint-disable-next-line @typescript-eslint/no-explicit-any parentCall, propagateFlags) { const grpcChannel = this.getChannelRef().getChannel(); return grpcChannel.createCall(method, deadline, host, parentCall, propagateFlags); } }; } exports.getGcpChannelFactoryClass = getGcpChannelFactoryClass; //# sourceMappingURL=gcp_channel_factory.js.map