UNPKG

etquia

Version:

Dummy package for the grpc-node repository

566 lines (525 loc) 22.8 kB
/* * Copyright 2020 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. * */ import { connectivityState as ConnectivityState, status as Status, Metadata, logVerbosity as LogVerbosity, experimental, StatusObject } from '@grpc/grpc-js'; import { getSingletonXdsClient, XdsClient, XdsClusterDropStats } from './xds-client'; import { ClusterLoadAssignment__Output } from './generated/envoy/config/endpoint/v3/ClusterLoadAssignment'; import { Locality__Output } from './generated/envoy/api/v2/core/Locality'; import { LocalitySubchannelAddress, PriorityChild, PriorityLoadBalancingConfig } from './load-balancer-priority'; import LoadBalancer = experimental.LoadBalancer; import ChannelControlHelper = experimental.ChannelControlHelper; import registerLoadBalancerType = experimental.registerLoadBalancerType; import LoadBalancingConfig = experimental.LoadBalancingConfig; import SubchannelAddress = experimental.SubchannelAddress; import subchannelAddressToString = experimental.subchannelAddressToString; import ChildLoadBalancerHandler = experimental.ChildLoadBalancerHandler; import UnavailablePicker = experimental.UnavailablePicker; import Picker = experimental.Picker; import PickResultType = experimental.PickResultType; import { validateLoadBalancingConfig } from '@grpc/grpc-js/build/src/experimental'; import { WeightedTarget, WeightedTargetLoadBalancingConfig } from './load-balancer-weighted-target'; import { LrsLoadBalancingConfig } from './load-balancer-lrs'; import { Watcher } from './xds-stream-state/xds-stream-state'; import Filter = experimental.Filter; import BaseFilter = experimental.BaseFilter; import FilterFactory = experimental.FilterFactory; import CallStream = experimental.CallStream; import OutlierDetectionLoadBalancingConfig = experimental.OutlierDetectionLoadBalancingConfig; import { EXPERIMENTAL_OUTLIER_DETECTION } from './environment'; const TRACER_NAME = 'eds_balancer'; function trace(text: string): void { experimental.trace(LogVerbosity.DEBUG, TRACER_NAME, text); } const TYPE_NAME = 'eds'; function localityToName(locality: Locality__Output) { return `{region=${locality.region},zone=${locality.zone},sub_zone=${locality.sub_zone}}`; } const DEFAULT_MAX_CONCURRENT_REQUESTS = 1024; export class EdsLoadBalancingConfig implements LoadBalancingConfig { private maxConcurrentRequests: number; getLoadBalancerName(): string { return TYPE_NAME; } toJsonObject(): object { const jsonObj: {[key: string]: any} = { cluster: this.cluster, locality_picking_policy: this.localityPickingPolicy.map(policy => policy.toJsonObject()), endpoint_picking_policy: this.endpointPickingPolicy.map(policy => policy.toJsonObject()), max_concurrent_requests: this.maxConcurrentRequests }; if (this.edsServiceName !== undefined) { jsonObj.eds_service_name = this.edsServiceName; } if (this.lrsLoadReportingServerName !== undefined) { jsonObj.lrs_load_reporting_server_name = this.lrsLoadReportingServerName; } if (this.outlierDetection !== undefined) { jsonObj.outlier_detection = this.outlierDetection.toJsonObject(); } return { [TYPE_NAME]: jsonObj }; } constructor(private cluster: string, private localityPickingPolicy: LoadBalancingConfig[], private endpointPickingPolicy: LoadBalancingConfig[], private edsServiceName?: string, private lrsLoadReportingServerName?: string, maxConcurrentRequests?: number, private outlierDetection?: OutlierDetectionLoadBalancingConfig) { this.maxConcurrentRequests = maxConcurrentRequests ?? DEFAULT_MAX_CONCURRENT_REQUESTS; } getCluster() { return this.cluster; } getLocalityPickingPolicy() { return this.localityPickingPolicy; } getEndpointPickingPolicy() { return this.endpointPickingPolicy; } getEdsServiceName() { return this.edsServiceName; } getLrsLoadReportingServerName() { return this.lrsLoadReportingServerName; } getMaxConcurrentRequests() { return this.maxConcurrentRequests; } getOutlierDetection() { return this.outlierDetection; } static createFromJson(obj: any): EdsLoadBalancingConfig { if (!('cluster' in obj && typeof obj.cluster === 'string')) { throw new Error('eds config must have a string field cluster'); } if (!('locality_picking_policy' in obj && Array.isArray(obj.locality_picking_policy))) { throw new Error('eds config must have a locality_picking_policy array'); } if (!('endpoint_picking_policy' in obj && Array.isArray(obj.endpoint_picking_policy))) { throw new Error('eds config must have an endpoint_picking_policy array'); } if ('eds_service_name' in obj && !(obj.eds_service_name === undefined || typeof obj.eds_service_name === 'string')) { throw new Error('eds config eds_service_name field must be a string if provided'); } if ('lrs_load_reporting_server_name' in obj && (!obj.lrs_load_reporting_server_name === undefined || typeof obj.lrs_load_reporting_server_name === 'string')) { throw new Error('eds config lrs_load_reporting_server_name must be a string if provided'); } if ('max_concurrent_requests' in obj && (!obj.max_concurrent_requests === undefined || typeof obj.max_concurrent_requests === 'number')) { throw new Error('eds config max_concurrent_requests must be a number if provided'); } let validatedOutlierDetectionConfig: OutlierDetectionLoadBalancingConfig | undefined = undefined; if (EXPERIMENTAL_OUTLIER_DETECTION) { if ('outlier_detection' in obj) { const outlierDetectionConfig = validateLoadBalancingConfig(obj.outlier_detection); if (!(outlierDetectionConfig instanceof OutlierDetectionLoadBalancingConfig)) { throw new Error('eds config outlier_detection must be a valid outlier detection config if provided'); } validatedOutlierDetectionConfig = outlierDetectionConfig; } } return new EdsLoadBalancingConfig(obj.cluster, obj.locality_picking_policy.map(validateLoadBalancingConfig), obj.endpoint_picking_policy.map(validateLoadBalancingConfig), obj.eds_service_name, obj.lrs_load_reporting_server_name, obj.max_concurrent_requests, validatedOutlierDetectionConfig); } } class CallEndTrackingFilter extends BaseFilter implements Filter { constructor(private onCallEnd: () => void) { super(); } receiveTrailers(status: StatusObject) { this.onCallEnd(); return status; } } class CallTrackingFilterFactory implements FilterFactory<CallEndTrackingFilter> { constructor(private onCallEnd: () => void) {} createFilter(callStream: CallStream) { return new CallEndTrackingFilter(this.onCallEnd); } } /** * This class load balances over a cluster by making an EDS request and then * transforming the result into a configuration for another load balancing * policy. */ export class EdsLoadBalancer implements LoadBalancer { /** * The child load balancer that will handle balancing the results of the EDS * requests. */ private childBalancer: ChildLoadBalancerHandler; private edsServiceName: string | null = null; private watcher: Watcher<ClusterLoadAssignment__Output>; /** * Indicates whether the watcher has already been passed to the xdsClient * and is getting updates. */ private isWatcherActive = false; private lastestConfig: EdsLoadBalancingConfig | null = null; private latestAttributes: { [key: string]: unknown } = {}; private latestEdsUpdate: ClusterLoadAssignment__Output | null = null; /** * The priority of each locality the last time we got an update. */ private localityPriorities: Map<string, number> = new Map<string, number>(); /** * The name we assigned to each priority number the last time we got an * update. */ private priorityNames: string[] = []; private nextPriorityChildNumber = 0; private clusterDropStats: XdsClusterDropStats | null = null; private concurrentRequests: number = 0; constructor(private readonly channelControlHelper: ChannelControlHelper) { this.childBalancer = new ChildLoadBalancerHandler(experimental.createChildChannelControlHelper(this.channelControlHelper, { updateState: (connectivityState, originalPicker) => { if (this.latestEdsUpdate === null) { return; } const edsPicker: Picker = { pick: (pickArgs) => { const dropCategory = this.checkForDrop(); /* If we drop the call, it ends with an UNAVAILABLE status. * Otherwise, delegate picking the subchannel to the child * balancer. */ if (dropCategory === null) { const originalPick = originalPicker.pick(pickArgs); const trackingFilterFactory: FilterFactory<Filter> = new CallTrackingFilterFactory(() => { this.concurrentRequests -= 1; }); return { pickResultType: originalPick.pickResultType, status: originalPick.status, subchannel: originalPick.subchannel, onCallStarted: () => { originalPick.onCallStarted?.(); this.concurrentRequests += 1; }, extraFilterFactories: originalPick.extraFilterFactories.concat(trackingFilterFactory) }; } else { let details: string; if (dropCategory === true) { details = 'Call dropped by load balancing policy.'; this.clusterDropStats?.addUncategorizedCallDropped(); } else { details = `Call dropped by load balancing policy. Category: ${dropCategory}`; this.clusterDropStats?.addCallDropped(dropCategory); } return { pickResultType: PickResultType.DROP, status: { code: Status.UNAVAILABLE, details: details, metadata: new Metadata(), }, subchannel: null, extraFilterFactories: [], onCallStarted: null }; } }, }; this.channelControlHelper.updateState(connectivityState, edsPicker); }, })); this.watcher = { onValidUpdate: (update) => { trace('Received EDS update for ' + this.edsServiceName + ': ' + JSON.stringify(update, undefined, 2)); this.latestEdsUpdate = update; this.updateChild(); }, onResourceDoesNotExist: () => { this.isWatcherActive = false; this.channelControlHelper.updateState(ConnectivityState.TRANSIENT_FAILURE, new UnavailablePicker({code: Status.UNAVAILABLE, details: 'EDS resource does not exist', metadata: new Metadata()})); this.childBalancer.destroy(); }, onTransientError: (status) => { if (this.latestEdsUpdate === null) { channelControlHelper.updateState( ConnectivityState.TRANSIENT_FAILURE, new UnavailablePicker({ code: Status.UNAVAILABLE, details: `xDS request failed with error ${status.details}`, metadata: new Metadata(), }) ); } }, }; } /** * Check whether a single call should be dropped according to the current * policy, based on randomly chosen numbers. Returns the drop category if * the call should be dropped, and null otherwise. true is a valid * output, as a sentinel value indicating a drop with no category. */ private checkForDrop(): string | true | null { if (this.lastestConfig && this.concurrentRequests >= this.lastestConfig.getMaxConcurrentRequests()) { return true; } if (!this.latestEdsUpdate?.policy) { return null; } /* The drop_overloads policy is a list of pairs of category names and * probabilities. For each one, if the random number is within that * probability range, we drop the call citing that category. Otherwise, the * call proceeds as usual. */ for (const dropOverload of this.latestEdsUpdate.policy.drop_overloads) { if (!dropOverload.drop_percentage) { continue; } let randNum: number; switch (dropOverload.drop_percentage.denominator) { case 'HUNDRED': randNum = Math.random() * 100; break; case 'TEN_THOUSAND': randNum = Math.random() * 10_000; break; case 'MILLION': randNum = Math.random() * 1_000_000; break; default: continue; } if (randNum < dropOverload.drop_percentage.numerator) { return dropOverload.category; } } return null; } /** * Should be called when this balancer gets a new config and when the * XdsClient returns a new ClusterLoadAssignment. */ private updateChild() { if (!(this.lastestConfig && this.latestEdsUpdate)) { return; } /** * Maps each priority number to the list of localities with that priority, * and the list of addresses associated with each locality. */ const priorityList: { locality: Locality__Output; weight: number; addresses: SubchannelAddress[]; }[][] = []; /** * New replacement for this.localityPriorities, mapping locality names to * priority values. The replacement occurrs at the end of this method. */ const newLocalityPriorities: Map<string, number> = new Map< string, number >(); /* We are given a list of localities, each of which has a priority. This * loop consolidates localities into buckets by priority, while also * simplifying the data structure to make the later steps simpler */ for (const endpoint of this.latestEdsUpdate.endpoints) { if (!endpoint.load_balancing_weight) { continue; } const addresses: SubchannelAddress[] = endpoint.lb_endpoints.filter(lbEndpoint => lbEndpoint.health_status === 'UNKNOWN' || lbEndpoint.health_status === 'HEALTHY').map( (lbEndpoint) => { /* The validator in the XdsClient class ensures that each endpoint has * a socket_address with an IP address and a port_value. */ const socketAddress = lbEndpoint.endpoint!.address!.socket_address!; return { host: socketAddress.address!, port: socketAddress.port_value!, }; } ); if (addresses.length > 0) { let localityArray = priorityList[endpoint.priority]; if (localityArray === undefined) { localityArray = []; priorityList[endpoint.priority] = localityArray; } localityArray.push({ locality: endpoint.locality!, addresses: addresses, weight: endpoint.load_balancing_weight.value, }); newLocalityPriorities.set( localityToName(endpoint.locality!), endpoint.priority ); } } const newPriorityNames: string[] = []; const addressList: LocalitySubchannelAddress[] = []; const priorityChildren: Map<string, PriorityChild> = new Map< string, PriorityChild >(); /* The algorithm here is as follows: for each priority we are given, from * high to low: * - If the previous mapping had any of the same localities at the same or * a lower priority, use the matching name from the highest such * priority, unless the new mapping has already used that name. * - Otherwise, construct a new name using this.nextPriorityChildNumber. */ for (const [priority, localityArray] of priorityList.entries()) { // Skip priorities that have no localities with healthy endpoints if (localityArray === undefined) { continue; } /** * Highest (smallest number) priority value that any of the localities in * this locality array had a in the previous mapping. */ let highestOldPriority = Infinity; for (const localityObj of localityArray) { const oldPriority = this.localityPriorities.get( localityToName(localityObj.locality) ); if ( oldPriority !== undefined && oldPriority >= priority && oldPriority < highestOldPriority ) { highestOldPriority = oldPriority; } } let newPriorityName: string; if (highestOldPriority === Infinity) { /* No existing priority at or below the same number as the priority we * are looking at had any of the localities in this priority. So, we * use a new name. */ newPriorityName = `child${this.nextPriorityChildNumber++}`; } else { const newName = this.priorityNames[highestOldPriority]; if (newPriorityNames.indexOf(newName) < 0) { newPriorityName = newName; } else { newPriorityName = `child${this.nextPriorityChildNumber++}`; } } newPriorityNames[priority] = newPriorityName; const childTargets: Map<string, WeightedTarget> = new Map< string, WeightedTarget >(); for (const localityObj of localityArray) { /* Use the endpoint picking policy from the config, default to * round_robin. */ const endpointPickingPolicy: LoadBalancingConfig[] = [ ...this.lastestConfig.getEndpointPickingPolicy(), validateLoadBalancingConfig({ round_robin: {} }), ]; let childPolicy: LoadBalancingConfig[]; if (this.lastestConfig.getLrsLoadReportingServerName() !== undefined) { childPolicy = [new LrsLoadBalancingConfig(this.lastestConfig.getCluster(), this.lastestConfig.getEdsServiceName() ?? '', this.lastestConfig.getLrsLoadReportingServerName()!, localityObj.locality, endpointPickingPolicy)]; } else { childPolicy = endpointPickingPolicy; } childTargets.set(localityToName(localityObj.locality), { weight: localityObj.weight, child_policy: childPolicy, }); for (const address of localityObj.addresses) { addressList.push({ localityPath: [ newPriorityName, localityToName(localityObj.locality), ], ...address, }); } } const weightedTargetConfig = new WeightedTargetLoadBalancingConfig(childTargets); let outlierDetectionConfig: OutlierDetectionLoadBalancingConfig | undefined; if (EXPERIMENTAL_OUTLIER_DETECTION) { outlierDetectionConfig = this.lastestConfig.getOutlierDetection()?.copyWithChildPolicy([weightedTargetConfig]); } const priorityChildConfig = outlierDetectionConfig ?? weightedTargetConfig; priorityChildren.set(newPriorityName, { config: [priorityChildConfig], }); } /* Contract the priority names array if it is sparse. This config only * cares about the order of priorities, not their specific numbers */ const childConfig: PriorityLoadBalancingConfig = new PriorityLoadBalancingConfig(priorityChildren, newPriorityNames.filter((value) => value !== undefined)); trace('Child update addresses: ' + addressList.map(address => '(' + subchannelAddressToString(address) + ' path=' + address.localityPath + ')')); trace('Child update priority config: ' + JSON.stringify(childConfig.toJsonObject(), undefined, 2)); this.childBalancer.updateAddressList( addressList, childConfig, this.latestAttributes ); this.localityPriorities = newLocalityPriorities; this.priorityNames = newPriorityNames; } updateAddressList( addressList: SubchannelAddress[], lbConfig: LoadBalancingConfig, attributes: { [key: string]: unknown } ): void { if (!(lbConfig instanceof EdsLoadBalancingConfig)) { trace('Discarding address list update with unrecognized config ' + JSON.stringify(lbConfig.toJsonObject(), undefined, 2)); return; } trace('Received update with config: ' + JSON.stringify(lbConfig, undefined, 2)); this.lastestConfig = lbConfig; this.latestAttributes = attributes; const newEdsServiceName = lbConfig.getEdsServiceName() ?? lbConfig.getCluster(); /* If the name is changing, disable the old watcher before adding the new * one */ if (this.isWatcherActive && this.edsServiceName !== newEdsServiceName) { trace('Removing old endpoint watcher for edsServiceName ' + this.edsServiceName) getSingletonXdsClient().removeEndpointWatcher(this.edsServiceName!, this.watcher); /* Setting isWatcherActive to false here lets us have one code path for * calling addEndpointWatcher */ this.isWatcherActive = false; /* If we have a new name, the latestEdsUpdate does not correspond to * the new config, so it is no longer valid */ this.latestEdsUpdate = null; } this.edsServiceName = newEdsServiceName; if (!this.isWatcherActive) { trace('Adding new endpoint watcher for edsServiceName ' + this.edsServiceName); getSingletonXdsClient().addEndpointWatcher(this.edsServiceName, this.watcher); this.isWatcherActive = true; } if (lbConfig.getLrsLoadReportingServerName()) { this.clusterDropStats = getSingletonXdsClient().addClusterDropStats( lbConfig.getLrsLoadReportingServerName()!, lbConfig.getCluster(), lbConfig.getEdsServiceName() ?? '' ); } /* If updateAddressList is called after receiving an update and the update * is still valid, we want to update the child config with the information * in the new EdsLoadBalancingConfig. */ this.updateChild(); } exitIdle(): void { this.childBalancer.exitIdle(); } resetBackoff(): void { this.childBalancer.resetBackoff(); } destroy(): void { trace('Destroying load balancer with edsServiceName ' + this.edsServiceName); if (this.edsServiceName) { getSingletonXdsClient().removeEndpointWatcher(this.edsServiceName, this.watcher); } this.childBalancer.destroy(); } getTypeName(): string { return TYPE_NAME; } } export function setup() { registerLoadBalancerType(TYPE_NAME, EdsLoadBalancer, EdsLoadBalancingConfig); }