diff --git a/packages/grpc-js-xds/src/duration.ts b/packages/grpc-js-xds/src/duration.ts new file mode 100644 index 000000000..07f33651f --- /dev/null +++ b/packages/grpc-js-xds/src/duration.ts @@ -0,0 +1,33 @@ +/* + * Copyright 2023 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 { experimental } from '@grpc/grpc-js'; +import { Duration__Output } from './generated/google/protobuf/Duration'; +import Duration = experimental.Duration; + +/** + * Convert a Duration protobuf message object to a Duration object as used in + * the ServiceConfig definition. The difference is that the protobuf message + * defines seconds as a long, which is represented as a string in JavaScript, + * and the one used in the service config defines it as a number. + * @param duration + */ +export function protoDurationToDuration(duration: Duration__Output): Duration { + return { + seconds: Number.parseInt(duration.seconds), + nanos: duration.nanos + }; +} diff --git a/packages/grpc-js-xds/src/load-balancer-cds.ts b/packages/grpc-js-xds/src/load-balancer-cds.ts index 6f791299c..3ebcbbdf6 100644 --- a/packages/grpc-js-xds/src/load-balancer-cds.ts +++ b/packages/grpc-js-xds/src/load-balancer-cds.ts @@ -15,7 +15,7 @@ * */ -import { connectivityState, status, Metadata, logVerbosity, experimental } from '@grpc/grpc-js'; +import { connectivityState, status, Metadata, logVerbosity, experimental, LoadBalancingConfig } from '@grpc/grpc-js'; import { getSingletonXdsClient, Watcher, XdsClient } from './xds-client'; import { Cluster__Output } from './generated/envoy/config/cluster/v3/Cluster'; import SubchannelAddress = experimental.SubchannelAddress; @@ -24,17 +24,18 @@ import ChildLoadBalancerHandler = experimental.ChildLoadBalancerHandler; import LoadBalancer = experimental.LoadBalancer; import ChannelControlHelper = experimental.ChannelControlHelper; import registerLoadBalancerType = experimental.registerLoadBalancerType; -import LoadBalancingConfig = experimental.LoadBalancingConfig; -import OutlierDetectionLoadBalancingConfig = experimental.OutlierDetectionLoadBalancingConfig; +import TypedLoadBalancingConfig = experimental.TypedLoadBalancingConfig; import SuccessRateEjectionConfig = experimental.SuccessRateEjectionConfig; import FailurePercentageEjectionConfig = experimental.FailurePercentageEjectionConfig; import QueuePicker = experimental.QueuePicker; +import OutlierDetectionRawConfig = experimental.OutlierDetectionRawConfig; +import parseLoadBalancingConfig = experimental.parseLoadBalancingConfig; import { OutlierDetection__Output } from './generated/envoy/config/cluster/v3/OutlierDetection'; import { Duration__Output } from './generated/google/protobuf/Duration'; import { EXPERIMENTAL_OUTLIER_DETECTION } from './environment'; -import { DiscoveryMechanism, XdsClusterResolverChildPolicyHandler, XdsClusterResolverLoadBalancingConfig } from './load-balancer-xds-cluster-resolver'; +import { DiscoveryMechanism, XdsClusterResolverChildPolicyHandler } from './load-balancer-xds-cluster-resolver'; import { CLUSTER_CONFIG_TYPE_URL, decodeSingleResource } from './resources'; -import { CdsUpdate, ClusterResourceType, OutlierDetectionUpdate } from './xds-resource-type/cluster-resource-type'; +import { CdsUpdate, ClusterResourceType } from './xds-resource-type/cluster-resource-type'; const TRACER_NAME = 'cds_balancer'; @@ -44,7 +45,7 @@ function trace(text: string): void { const TYPE_NAME = 'cds'; -export class CdsLoadBalancingConfig implements LoadBalancingConfig { +class CdsLoadBalancingConfig implements TypedLoadBalancingConfig { getLoadBalancerName(): string { return TYPE_NAME; } @@ -72,29 +73,6 @@ export class CdsLoadBalancingConfig implements LoadBalancingConfig { } } -function durationToMs(duration: Duration__Output): number { - return (Number(duration.seconds) * 1_000 + duration.nanos / 1_000_000) | 0; -} - -function translateOutlierDetectionConfig(outlierDetection: OutlierDetectionUpdate | undefined): OutlierDetectionLoadBalancingConfig | undefined { - if (!EXPERIMENTAL_OUTLIER_DETECTION) { - return undefined; - } - if (!outlierDetection) { - /* No-op outlier detection config, with all fields unset. */ - return new OutlierDetectionLoadBalancingConfig(null, null, null, null, null, null, []); - } - return new OutlierDetectionLoadBalancingConfig( - outlierDetection.intervalMs, - outlierDetection.baseEjectionTimeMs, - outlierDetection.maxEjectionTimeMs, - outlierDetection.maxEjectionPercent, - outlierDetection.successRateConfig, - outlierDetection.failurePercentageConfig, - [] - ); -} - interface ClusterEntry { watcher: Watcher; latestUpdate?: CdsUpdate; @@ -133,7 +111,7 @@ function generateDiscoverymechanismForCdsUpdate(config: CdsUpdate): DiscoveryMec type: config.type, eds_service_name: config.edsServiceName, dns_hostname: config.dnsHostname, - outlier_detection: translateOutlierDetectionConfig(config.outlierDetectionUpdate) + outlier_detection: config.outlierDetectionUpdate }; } @@ -141,8 +119,8 @@ const RECURSION_DEPTH_LIMIT = 15; /** * Prerequisite: isClusterTreeFullyUpdated(tree, root) - * @param tree - * @param root + * @param tree + * @param root */ function getDiscoveryMechanismList(tree: ClusterTree, root: string): DiscoveryMechanism[] { const visited = new Set(); @@ -189,6 +167,11 @@ export class CdsLoadBalancer implements LoadBalancer { this.childBalancer = new XdsClusterResolverChildPolicyHandler(channelControlHelper); } + private reportError(errorMessage: string) { + trace('CDS cluster reporting error ' + errorMessage); + this.channelControlHelper.updateState(connectivityState.TRANSIENT_FAILURE, new UnavailablePicker({code: status.UNAVAILABLE, details: errorMessage, metadata: new Metadata()})); + } + private addCluster(cluster: string) { if (cluster in this.clusterTree) { return; @@ -208,19 +191,28 @@ export class CdsLoadBalancer implements LoadBalancer { try { discoveryMechanismList = getDiscoveryMechanismList(this.clusterTree, this.latestConfig!.getCluster()); } catch (e) { - this.channelControlHelper.updateState(connectivityState.TRANSIENT_FAILURE, new UnavailablePicker({code: status.UNAVAILABLE, details: e.message, metadata: new Metadata()})); + this.reportError((e as Error).message); + return; + } + const clusterResolverConfig: LoadBalancingConfig = { + xds_cluster_resolver: { + discovery_mechanisms: discoveryMechanismList, + locality_picking_policy: [], + endpoint_picking_policy: [] + } + }; + let parsedClusterResolverConfig: TypedLoadBalancingConfig; + try { + parsedClusterResolverConfig = parseLoadBalancingConfig(clusterResolverConfig); + } catch (e) { + this.reportError(`CDS cluster ${this.latestConfig?.getCluster()} child config parsing failed with error ${(e as Error).message}`); return; } - const clusterResolverConfig = new XdsClusterResolverLoadBalancingConfig( - discoveryMechanismList, - [], - [] - ); trace('Child update config: ' + JSON.stringify(clusterResolverConfig)); this.updatedChild = true; this.childBalancer.updateAddressList( [], - clusterResolverConfig, + parsedClusterResolverConfig, this.latestAttributes ); } @@ -231,20 +223,13 @@ export class CdsLoadBalancer implements LoadBalancer { this.clusterTree[cluster].latestUpdate = undefined; this.clusterTree[cluster].children = []; } - this.channelControlHelper.updateState(connectivityState.TRANSIENT_FAILURE, new UnavailablePicker({code: status.UNAVAILABLE, details: `CDS resource ${cluster} does not exist`, metadata: new Metadata()})); + this.reportError(`CDS resource ${cluster} does not exist`); this.childBalancer.destroy(); }, onError: (statusObj) => { if (!this.updatedChild) { trace('Transitioning to transient failure due to onError update for cluster' + cluster); - this.channelControlHelper.updateState( - connectivityState.TRANSIENT_FAILURE, - new UnavailablePicker({ - code: status.UNAVAILABLE, - details: `xDS request failed with error ${statusObj.details}`, - metadata: new Metadata(), - }) - ); + this.reportError(`xDS request failed with error ${statusObj.details}`); } } }); @@ -275,7 +260,7 @@ export class CdsLoadBalancer implements LoadBalancer { updateAddressList( addressList: SubchannelAddress[], - lbConfig: LoadBalancingConfig, + lbConfig: TypedLoadBalancingConfig, attributes: { [key: string]: unknown } ): void { if (!(lbConfig instanceof CdsLoadBalancingConfig)) { diff --git a/packages/grpc-js-xds/src/load-balancer-lrs.ts b/packages/grpc-js-xds/src/load-balancer-lrs.ts index a2deb72c3..145b4deda 100644 --- a/packages/grpc-js-xds/src/load-balancer-lrs.ts +++ b/packages/grpc-js-xds/src/load-balancer-lrs.ts @@ -22,9 +22,7 @@ import { XdsClusterLocalityStats, XdsClient, getSingletonXdsClient } from './xds import LoadBalancer = experimental.LoadBalancer; import ChannelControlHelper = experimental.ChannelControlHelper; import registerLoadBalancerType = experimental.registerLoadBalancerType; -import getFirstUsableConfig = experimental.getFirstUsableConfig; import SubchannelAddress = experimental.SubchannelAddress; -import LoadBalancingConfig = experimental.LoadBalancingConfig; import ChildLoadBalancerHandler = experimental.ChildLoadBalancerHandler; import Picker = experimental.Picker; import PickArgs = experimental.PickArgs; @@ -34,11 +32,12 @@ import Filter = experimental.Filter; import BaseFilter = experimental.BaseFilter; import FilterFactory = experimental.FilterFactory; import Call = experimental.CallStream; -import validateLoadBalancingConfig = experimental.validateLoadBalancingConfig +import TypedLoadBalancingConfig = experimental.TypedLoadBalancingConfig; +import selectLbConfigFromList = experimental.selectLbConfigFromList; const TYPE_NAME = 'lrs'; -export class LrsLoadBalancingConfig implements LoadBalancingConfig { +class LrsLoadBalancingConfig implements TypedLoadBalancingConfig { getLoadBalancerName(): string { return TYPE_NAME; } @@ -49,12 +48,12 @@ export class LrsLoadBalancingConfig implements LoadBalancingConfig { eds_service_name: this.edsServiceName, lrs_load_reporting_server_name: this.lrsLoadReportingServer, locality: this.locality, - child_policy: this.childPolicy.map(policy => policy.toJsonObject()) + child_policy: [this.childPolicy.toJsonObject()] } } } - constructor(private clusterName: string, private edsServiceName: string, private lrsLoadReportingServer: XdsServerConfig, private locality: Locality__Output, private childPolicy: LoadBalancingConfig[]) {} + constructor(private clusterName: string, private edsServiceName: string, private lrsLoadReportingServer: XdsServerConfig, private locality: Locality__Output, private childPolicy: TypedLoadBalancingConfig) {} getClusterName() { return this.clusterName; @@ -98,11 +97,15 @@ export class LrsLoadBalancingConfig implements LoadBalancingConfig { if (!('child_policy' in obj && Array.isArray(obj.child_policy))) { throw new Error('lrs config must have a child_policy array'); } + const childConfig = selectLbConfigFromList(obj.config); + if (!childConfig) { + throw new Error('lrs config child_policy parsing failed'); + } return new LrsLoadBalancingConfig(obj.cluster_name, obj.eds_service_name, validateXdsServerConfig(obj.lrs_load_reporting_server), { region: obj.locality.region ?? '', zone: obj.locality.zone ?? '', sub_zone: obj.locality.sub_zone ?? '' - }, obj.child_policy.map(validateLoadBalancingConfig)); + }, childConfig); } } @@ -161,7 +164,7 @@ export class LrsLoadBalancer implements LoadBalancer { updateAddressList( addressList: SubchannelAddress[], - lbConfig: LoadBalancingConfig, + lbConfig: TypedLoadBalancingConfig, attributes: { [key: string]: unknown } ): void { if (!(lbConfig instanceof LrsLoadBalancingConfig)) { @@ -173,11 +176,7 @@ export class LrsLoadBalancer implements LoadBalancer { lbConfig.getEdsServiceName(), lbConfig.getLocality() ); - const childPolicy: LoadBalancingConfig = getFirstUsableConfig( - lbConfig.getChildPolicy(), - true - ); - this.childBalancer.updateAddressList(addressList, childPolicy, attributes); + this.childBalancer.updateAddressList(addressList, lbConfig.getChildPolicy(), attributes); } exitIdle(): void { this.childBalancer.exitIdle(); diff --git a/packages/grpc-js-xds/src/load-balancer-priority.ts b/packages/grpc-js-xds/src/load-balancer-priority.ts index a9d03d0a6..4d26a0f41 100644 --- a/packages/grpc-js-xds/src/load-balancer-priority.ts +++ b/packages/grpc-js-xds/src/load-balancer-priority.ts @@ -15,19 +15,18 @@ * */ -import { connectivityState as ConnectivityState, status as Status, Metadata, logVerbosity as LogVerbosity, experimental, ChannelOptions } from '@grpc/grpc-js'; -import validateLoadBalancingConfig = experimental.validateLoadBalancingConfig; +import { connectivityState as ConnectivityState, status as Status, Metadata, logVerbosity as LogVerbosity, experimental, LoadBalancingConfig } from '@grpc/grpc-js'; import LoadBalancer = experimental.LoadBalancer; import ChannelControlHelper = experimental.ChannelControlHelper; -import getFirstUsableConfig = experimental.getFirstUsableConfig; import registerLoadBalancerType = experimental.registerLoadBalancerType; import SubchannelAddress = experimental.SubchannelAddress; import subchannelAddressToString = experimental.subchannelAddressToString; -import LoadBalancingConfig = experimental.LoadBalancingConfig; +import TypedLoadBalancingConfig = experimental.TypedLoadBalancingConfig; import Picker = experimental.Picker; import QueuePicker = experimental.QueuePicker; import UnavailablePicker = experimental.UnavailablePicker; import ChildLoadBalancerHandler = experimental.ChildLoadBalancerHandler; +import selectLbConfigFromList = experimental.selectLbConfigFromList; const TRACER_NAME = 'priority'; @@ -50,12 +49,31 @@ export function isLocalitySubchannelAddress( return Array.isArray((address as LocalitySubchannelAddress).localityPath); } -export interface PriorityChild { +/** + * Type of the config for an individual child in the JSON representation of + * a priority LB policy config. + */ +export interface PriorityChildRaw { config: LoadBalancingConfig[]; ignore_reresolution_requests: boolean; } -export class PriorityLoadBalancingConfig implements LoadBalancingConfig { +/** + * The JSON representation of the config for the priority LB policy. The + * LoadBalancingConfig for a priority policy should have the form + * { priority: PriorityRawConfig } + */ +export interface PriorityRawConfig { + children: {[name: string]: PriorityChildRaw}; + priorities: string[]; +} + +interface PriorityChild { + config: TypedLoadBalancingConfig; + ignore_reresolution_requests: boolean; +} + +class PriorityLoadBalancingConfig implements TypedLoadBalancingConfig { getLoadBalancerName(): string { return TYPE_NAME; } @@ -63,7 +81,7 @@ export class PriorityLoadBalancingConfig implements LoadBalancingConfig { const childrenField: {[key: string]: object} = {} for (const [childName, childValue] of this.children.entries()) { childrenField[childName] = { - config: childValue.config.map(value => value.toJsonObject()) + config: [childValue.config.toJsonObject()] }; } return { @@ -93,7 +111,7 @@ export class PriorityLoadBalancingConfig implements LoadBalancingConfig { throw new Error('Priority config must have a priorities list'); } const childrenMap: Map = new Map(); - for (const childName of obj.children) { + for (const childName of Object.keys(obj.children)) { const childObj = obj.children[childName] if (!('config' in childObj && Array.isArray(childObj.config))) { throw new Error(`Priority child ${childName} must have a config list`); @@ -101,8 +119,12 @@ export class PriorityLoadBalancingConfig implements LoadBalancingConfig { if (!('ignore_reresolution_requests' in childObj && typeof childObj.ignore_reresolution_requests === 'boolean')) { throw new Error(`Priority child ${childName} must have a boolean field ignore_reresolution_requests`); } + const childConfig = selectLbConfigFromList(childObj.config); + if (!childConfig) { + throw new Error(`Priority child ${childName} config parsing failed`); + } childrenMap.set(childName, { - config: childObj.config.map(validateLoadBalancingConfig), + config: childConfig, ignore_reresolution_requests: childObj.ignore_reresolution_requests }); } @@ -113,7 +135,7 @@ export class PriorityLoadBalancingConfig implements LoadBalancingConfig { interface PriorityChildBalancer { updateAddressList( addressList: SubchannelAddress[], - lbConfig: LoadBalancingConfig, + lbConfig: TypedLoadBalancingConfig, attributes: { [key: string]: unknown } ): void; exitIdle(): void; @@ -129,7 +151,7 @@ interface PriorityChildBalancer { interface UpdateArgs { subchannelAddress: SubchannelAddress[]; - lbConfig: LoadBalancingConfig; + lbConfig: TypedLoadBalancingConfig; ignoreReresolutionRequests: boolean; } @@ -193,7 +215,7 @@ export class PriorityLoadBalancer implements LoadBalancer { updateAddressList( addressList: SubchannelAddress[], - lbConfig: LoadBalancingConfig, + lbConfig: TypedLoadBalancingConfig, attributes: { [key: string]: unknown } ): void { this.childBalancer.updateAddressList(addressList, lbConfig, attributes); @@ -387,7 +409,7 @@ export class PriorityLoadBalancer implements LoadBalancer { updateAddressList( addressList: SubchannelAddress[], - lbConfig: LoadBalancingConfig, + lbConfig: TypedLoadBalancingConfig, attributes: { [key: string]: unknown } ): void { if (!(lbConfig instanceof PriorityLoadBalancingConfig)) { @@ -431,23 +453,20 @@ export class PriorityLoadBalancer implements LoadBalancer { /* Pair up the new child configs with the corresponding address lists, and * update all existing children with their new configs */ for (const [childName, childConfig] of lbConfig.getChildren()) { - const chosenChildConfig = getFirstUsableConfig(childConfig.config); - if (chosenChildConfig !== null) { - const childAddresses = childAddressMap.get(childName) ?? []; - trace('Assigning child ' + childName + ' address list ' + childAddresses.map(address => '(' + subchannelAddressToString(address) + ' path=' + address.localityPath + ')')) - this.latestUpdates.set(childName, { - subchannelAddress: childAddresses, - lbConfig: chosenChildConfig, - ignoreReresolutionRequests: childConfig.ignore_reresolution_requests - }); - const existingChild = this.children.get(childName); - if (existingChild !== undefined) { - existingChild.updateAddressList( - childAddresses, - chosenChildConfig, - attributes - ); - } + const childAddresses = childAddressMap.get(childName) ?? []; + trace('Assigning child ' + childName + ' address list ' + childAddresses.map(address => '(' + subchannelAddressToString(address) + ' path=' + address.localityPath + ')')) + this.latestUpdates.set(childName, { + subchannelAddress: childAddresses, + lbConfig: childConfig.config, + ignoreReresolutionRequests: childConfig.ignore_reresolution_requests + }); + const existingChild = this.children.get(childName); + if (existingChild !== undefined) { + existingChild.updateAddressList( + childAddresses, + childConfig.config, + attributes + ); } } // Deactivate all children that are no longer in the priority list diff --git a/packages/grpc-js-xds/src/load-balancer-weighted-target.ts b/packages/grpc-js-xds/src/load-balancer-weighted-target.ts index 7cd92d98b..231f3b179 100644 --- a/packages/grpc-js-xds/src/load-balancer-weighted-target.ts +++ b/packages/grpc-js-xds/src/load-balancer-weighted-target.ts @@ -15,12 +15,11 @@ * */ -import { connectivityState as ConnectivityState, status as Status, Metadata, logVerbosity, experimental } from "@grpc/grpc-js"; +import { connectivityState as ConnectivityState, status as Status, Metadata, logVerbosity, experimental, LoadBalancingConfig } from "@grpc/grpc-js"; import { isLocalitySubchannelAddress, LocalitySubchannelAddress } from "./load-balancer-priority"; -import LoadBalancingConfig = experimental.LoadBalancingConfig; +import TypedLoadBalancingConfig = experimental.TypedLoadBalancingConfig; import LoadBalancer = experimental.LoadBalancer; import ChannelControlHelper = experimental.ChannelControlHelper; -import getFirstUsableConfig = experimental.getFirstUsableConfig; import registerLoadBalancerType = experimental.registerLoadBalancerType; import ChildLoadBalancerHandler = experimental.ChildLoadBalancerHandler; import Picker = experimental.Picker; @@ -30,7 +29,7 @@ import QueuePicker = experimental.QueuePicker; import UnavailablePicker = experimental.UnavailablePicker; import SubchannelAddress = experimental.SubchannelAddress; import subchannelAddressToString = experimental.subchannelAddressToString; -import validateLoadBalancingConfig = experimental.validateLoadBalancingConfig; +import selectLbConfigFromList = experimental.selectLbConfigFromList; const TRACER_NAME = 'weighted_target'; @@ -42,12 +41,30 @@ const TYPE_NAME = 'weighted_target'; const DEFAULT_RETENTION_INTERVAL_MS = 15 * 60 * 1000; - export interface WeightedTarget { +/** + * Type of the config for an individual child in the JSON representation of + * a weighted target LB policy config. + */ +export interface WeightedTargetRaw { weight: number; child_policy: LoadBalancingConfig[]; } -export class WeightedTargetLoadBalancingConfig implements LoadBalancingConfig { +/** + * The JSON representation of the config for the weighted target LB policy. The + * LoadBalancingConfig for a weighted target policy should have the form + * { weighted_target: WeightedTargetRawConfig } + */ +export interface WeightedTargetRawConfig { + targets: {[name: string]: WeightedTargetRaw }; +} + +interface WeightedTarget { + weight: number; + child_policy: TypedLoadBalancingConfig; +} + +class WeightedTargetLoadBalancingConfig implements TypedLoadBalancingConfig { getLoadBalancerName(): string { return TYPE_NAME; } @@ -64,7 +81,7 @@ export class WeightedTargetLoadBalancingConfig implements LoadBalancingConfig { for (const [targetName, targetValue] of this.targets.entries()) { targetsField[targetName] = { weight: targetValue.weight, - child_policy: targetValue.child_policy.map(policy => policy.toJsonObject()) + child_policy: [targetValue.child_policy.toJsonObject()] }; } return { @@ -79,7 +96,7 @@ export class WeightedTargetLoadBalancingConfig implements LoadBalancingConfig { if (!('targets' in obj && obj.targets !== null && typeof obj.targets === 'object')) { throw new Error('Weighted target config must have a targets map'); } - for (const key of obj.targets) { + for (const key of Object.keys(obj.targets)) { const targetObj = obj.targets[key]; if (!('weight' in targetObj && typeof targetObj.weight === 'number')) { throw new Error(`Weighted target ${key} must have a numeric weight`); @@ -87,9 +104,13 @@ export class WeightedTargetLoadBalancingConfig implements LoadBalancingConfig { if (!('child_policy' in targetObj && Array.isArray(targetObj.child_policy))) { throw new Error(`Weighted target ${key} must have a child_policy array`); } + const childConfig = selectLbConfigFromList(targetObj.child_policy); + if (!childConfig) { + throw new Error(`Weighted target ${key} config parsing failed`); + } const validatedTarget: WeightedTarget = { weight: targetObj.weight, - child_policy: targetObj.child_policy.map(validateLoadBalancingConfig) + child_policy: childConfig } targetsMap.set(key, validatedTarget); } @@ -171,10 +192,7 @@ export class WeightedTargetLoadBalancer implements LoadBalancer { updateAddressList(addressList: SubchannelAddress[], lbConfig: WeightedTarget, attributes: { [key: string]: unknown; }): void { this.weight = lbConfig.weight; - const childConfig = getFirstUsableConfig(lbConfig.child_policy); - if (childConfig !== null) { - this.childBalancer.updateAddressList(addressList, childConfig, attributes); - } + this.childBalancer.updateAddressList(addressList, lbConfig.child_policy, attributes); } exitIdle(): void { this.childBalancer.exitIdle(); @@ -301,7 +319,7 @@ export class WeightedTargetLoadBalancer implements LoadBalancer { this.channelControlHelper.updateState(connectivityState, picker); } - updateAddressList(addressList: SubchannelAddress[], lbConfig: LoadBalancingConfig, attributes: { [key: string]: unknown; }): void { + updateAddressList(addressList: SubchannelAddress[], lbConfig: TypedLoadBalancingConfig, attributes: { [key: string]: unknown; }): void { if (!(lbConfig instanceof WeightedTargetLoadBalancingConfig)) { // Reject a config of the wrong type trace('Discarding address list update with unrecognized config ' + JSON.stringify(lbConfig.toJsonObject(), undefined, 2)); @@ -384,4 +402,4 @@ export class WeightedTargetLoadBalancer implements LoadBalancer { export function setup() { registerLoadBalancerType(TYPE_NAME, WeightedTargetLoadBalancer, WeightedTargetLoadBalancingConfig); -} \ No newline at end of file +} diff --git a/packages/grpc-js-xds/src/load-balancer-xds-cluster-impl.ts b/packages/grpc-js-xds/src/load-balancer-xds-cluster-impl.ts index edfd52016..e5db45afd 100644 --- a/packages/grpc-js-xds/src/load-balancer-xds-cluster-impl.ts +++ b/packages/grpc-js-xds/src/load-balancer-xds-cluster-impl.ts @@ -19,8 +19,6 @@ import { experimental, logVerbosity, status as Status, Metadata, connectivitySta import { validateXdsServerConfig, XdsServerConfig } from "./xds-bootstrap"; import { getSingletonXdsClient, XdsClient, XdsClusterDropStats } from "./xds-client"; -import LoadBalancingConfig = experimental.LoadBalancingConfig; -import validateLoadBalancingConfig = experimental.validateLoadBalancingConfig; import LoadBalancer = experimental.LoadBalancer; import registerLoadBalancerType = experimental.registerLoadBalancerType; import SubchannelAddress = experimental.SubchannelAddress; @@ -31,7 +29,8 @@ import PickResultType = experimental.PickResultType; import ChannelControlHelper = experimental.ChannelControlHelper; import ChildLoadBalancerHandler = experimental.ChildLoadBalancerHandler; import createChildChannelControlHelper = experimental.createChildChannelControlHelper; -import getFirstUsableConfig = experimental.getFirstUsableConfig; +import TypedLoadBalancingConfig = experimental.TypedLoadBalancingConfig; +import selectLbConfigFromList = experimental.selectLbConfigFromList; const TRACER_NAME = 'xds_cluster_impl'; @@ -58,7 +57,7 @@ function validateDropCategory(obj: any): DropCategory { return obj; } -export class XdsClusterImplLoadBalancingConfig implements LoadBalancingConfig { +class XdsClusterImplLoadBalancingConfig implements TypedLoadBalancingConfig { private maxConcurrentRequests: number; getLoadBalancerName(): string { return TYPE_NAME; @@ -67,7 +66,7 @@ export class XdsClusterImplLoadBalancingConfig implements LoadBalancingConfig { const jsonObj: {[key: string]: any} = { cluster: this.cluster, drop_categories: this.dropCategories, - child_policy: this.childPolicy.map(policy => policy.toJsonObject()), + child_policy: [this.childPolicy.toJsonObject()], max_concurrent_requests: this.maxConcurrentRequests }; if (this.edsServiceName !== undefined) { @@ -81,7 +80,7 @@ export class XdsClusterImplLoadBalancingConfig implements LoadBalancingConfig { }; } - constructor(private cluster: string, private dropCategories: DropCategory[], private childPolicy: LoadBalancingConfig[], private edsServiceName?: string, private lrsLoadReportingServer?: XdsServerConfig, maxConcurrentRequests?: number) { + constructor(private cluster: string, private dropCategories: DropCategory[], private childPolicy: TypedLoadBalancingConfig, private edsServiceName?: string, private lrsLoadReportingServer?: XdsServerConfig, maxConcurrentRequests?: number) { this.maxConcurrentRequests = maxConcurrentRequests ?? DEFAULT_MAX_CONCURRENT_REQUESTS; } @@ -125,7 +124,11 @@ export class XdsClusterImplLoadBalancingConfig implements LoadBalancingConfig { if (!('child_policy' in obj && Array.isArray(obj.child_policy))) { throw new Error('xds_cluster_impl config must have an array field child_policy'); } - return new XdsClusterImplLoadBalancingConfig(obj.cluster, obj.drop_categories.map(validateDropCategory), obj.child_policy.map(validateLoadBalancingConfig), obj.eds_service_name, obj.lrs_load_reporting_server ? validateXdsServerConfig(obj.lrs_load_reporting_server) : undefined, obj.max_concurrent_requests); + const childConfig = selectLbConfigFromList(obj.child_policy); + if (!childConfig) { + throw new Error('xds_cluster_impl config child_policy parsing failed'); + } + return new XdsClusterImplLoadBalancingConfig(obj.cluster, obj.drop_categories.map(validateDropCategory), childConfig, obj.eds_service_name, obj.lrs_load_reporting_server ? validateXdsServerConfig(obj.lrs_load_reporting_server) : undefined, obj.max_concurrent_requests); } } @@ -234,7 +237,7 @@ class XdsClusterImplBalancer implements LoadBalancer { } })); } - updateAddressList(addressList: SubchannelAddress[], lbConfig: LoadBalancingConfig, attributes: { [key: string]: unknown; }): void { + updateAddressList(addressList: SubchannelAddress[], lbConfig: TypedLoadBalancingConfig, attributes: { [key: string]: unknown; }): void { if (!(lbConfig instanceof XdsClusterImplLoadBalancingConfig)) { trace('Discarding address list update with unrecognized config ' + JSON.stringify(lbConfig.toJsonObject(), undefined, 2)); return; @@ -251,7 +254,7 @@ class XdsClusterImplBalancer implements LoadBalancer { ); } - this.childBalancer.updateAddressList(addressList, getFirstUsableConfig(lbConfig.getChildPolicy(), true), attributes); + this.childBalancer.updateAddressList(addressList, lbConfig.getChildPolicy(), attributes); } exitIdle(): void { this.childBalancer.exitIdle(); diff --git a/packages/grpc-js-xds/src/load-balancer-xds-cluster-manager.ts b/packages/grpc-js-xds/src/load-balancer-xds-cluster-manager.ts index bfdb4dccc..ce3207dfd 100644 --- a/packages/grpc-js-xds/src/load-balancer-xds-cluster-manager.ts +++ b/packages/grpc-js-xds/src/load-balancer-xds-cluster-manager.ts @@ -17,8 +17,7 @@ import { connectivityState as ConnectivityState, status as Status, experimental, logVerbosity, Metadata, status } from "@grpc/grpc-js/"; -import LoadBalancingConfig = experimental.LoadBalancingConfig; -import validateLoadBalancingConfig = experimental.validateLoadBalancingConfig; +import TypedLoadBalancingConfig = experimental.TypedLoadBalancingConfig; import LoadBalancer = experimental.LoadBalancer; import Picker = experimental.Picker; import PickResult = experimental.PickResult; @@ -28,8 +27,8 @@ import UnavailablePicker = experimental.UnavailablePicker; import QueuePicker = experimental.QueuePicker; import SubchannelAddress = experimental.SubchannelAddress; import ChildLoadBalancerHandler = experimental.ChildLoadBalancerHandler; -import getFirstUsableConfig = experimental.getFirstUsableConfig; import ChannelControlHelper = experimental.ChannelControlHelper; +import selectLbConfigFromList = experimental.selectLbConfigFromList; import registerLoadBalancerType = experimental.registerLoadBalancerType; const TRACER_NAME = 'xds_cluster_manager'; @@ -40,16 +39,12 @@ function trace(text: string): void { const TYPE_NAME = 'xds_cluster_manager'; -interface ClusterManagerChild { - child_policy: LoadBalancingConfig[]; -} - -export class XdsClusterManagerLoadBalancingConfig implements LoadBalancingConfig { +class XdsClusterManagerLoadBalancingConfig implements TypedLoadBalancingConfig { getLoadBalancerName(): string { return TYPE_NAME; } - constructor(private children: Map) {} + constructor(private children: Map) {} getChildren() { return this.children; @@ -57,9 +52,9 @@ export class XdsClusterManagerLoadBalancingConfig implements LoadBalancingConfig toJsonObject(): object { const childrenField: {[key: string]: object} = {}; - for (const [childName, childValue] of this.children.entries()) { + for (const [childName, childPolicy] of this.children.entries()) { childrenField[childName] = { - child_policy: childValue.child_policy.map(policy => policy.toJsonObject()) + child_policy: [childPolicy.toJsonObject()] }; } return { @@ -70,19 +65,20 @@ export class XdsClusterManagerLoadBalancingConfig implements LoadBalancingConfig } static createFromJson(obj: any): XdsClusterManagerLoadBalancingConfig { - const childrenMap: Map = new Map(); + const childrenMap: Map = new Map(); if (!('children' in obj && obj.children !== null && typeof obj.children === 'object')) { throw new Error('xds_cluster_manager config must have a children map'); } - for (const key of obj.children) { + for (const key of Object.keys(obj.children)) { const childObj = obj.children[key]; if (!('child_policy' in childObj && Array.isArray(childObj.child_policy))) { throw new Error(`xds_cluster_manager child ${key} must have a child_policy array`); } - const validatedChild = { - child_policy: childObj.child_policy.map(validateLoadBalancingConfig) - }; - childrenMap.set(key, validatedChild); + const childPolicy = selectLbConfigFromList(childObj.child_policy); + if (childPolicy === null) { + throw new Error(`xds_cluster_mananger child ${key} has no recognized sucessfully parsed child_policy`); + } + childrenMap.set(key, childPolicy); } return new XdsClusterManagerLoadBalancingConfig(childrenMap); } @@ -115,7 +111,7 @@ class XdsClusterManagerPicker implements Picker { } interface XdsClusterManagerChild { - updateAddressList(addressList: SubchannelAddress[], lbConfig: ClusterManagerChild, attributes: { [key: string]: unknown; }): void; + updateAddressList(addressList: SubchannelAddress[], childConfig: TypedLoadBalancingConfig, attributes: { [key: string]: unknown; }): void; exitIdle(): void; resetBackoff(): void; destroy(): void; @@ -146,11 +142,8 @@ class XdsClusterManager implements LoadBalancer { this.picker = picker; this.parent.maybeUpdateState(); } - updateAddressList(addressList: SubchannelAddress[], lbConfig: ClusterManagerChild, attributes: { [key: string]: unknown; }): void { - const childConfig = getFirstUsableConfig(lbConfig.child_policy); - if (childConfig !== null) { - this.childBalancer.updateAddressList(addressList, childConfig, attributes); - } + updateAddressList(addressList: SubchannelAddress[], childConfig: TypedLoadBalancingConfig, attributes: { [key: string]: unknown; }): void { + this.childBalancer.updateAddressList(addressList, childConfig, attributes); } exitIdle(): void { this.childBalancer.exitIdle(); @@ -241,8 +234,8 @@ class XdsClusterManager implements LoadBalancer { ); this.channelControlHelper.updateState(connectivityState, picker); } - - updateAddressList(addressList: SubchannelAddress[], lbConfig: LoadBalancingConfig, attributes: { [key: string]: unknown; }): void { + + updateAddressList(addressList: SubchannelAddress[], lbConfig: TypedLoadBalancingConfig, attributes: { [key: string]: unknown; }): void { if (!(lbConfig instanceof XdsClusterManagerLoadBalancingConfig)) { // Reject a config of the wrong type trace('Discarding address list update with unrecognized config ' + JSON.stringify(lbConfig.toJsonObject(), undefined, 2)); @@ -296,4 +289,4 @@ class XdsClusterManager implements LoadBalancer { export function setup() { registerLoadBalancerType(TYPE_NAME, XdsClusterManager, XdsClusterManagerLoadBalancingConfig); -} \ No newline at end of file +} diff --git a/packages/grpc-js-xds/src/load-balancer-xds-cluster-resolver.ts b/packages/grpc-js-xds/src/load-balancer-xds-cluster-resolver.ts index 40f67c476..f3c64aecb 100644 --- a/packages/grpc-js-xds/src/load-balancer-xds-cluster-resolver.ts +++ b/packages/grpc-js-xds/src/load-balancer-xds-cluster-resolver.ts @@ -15,29 +15,30 @@ * */ -import { experimental, logVerbosity } from "@grpc/grpc-js"; +import { LoadBalancingConfig, Metadata, connectivityState, experimental, logVerbosity, status } from "@grpc/grpc-js"; import { registerLoadBalancerType } from "@grpc/grpc-js/build/src/load-balancer"; import { EXPERIMENTAL_OUTLIER_DETECTION } from "./environment"; import { Locality__Output } from "./generated/envoy/config/core/v3/Locality"; import { ClusterLoadAssignment__Output } from "./generated/envoy/config/endpoint/v3/ClusterLoadAssignment"; -import { LrsLoadBalancingConfig } from "./load-balancer-lrs"; -import { LocalitySubchannelAddress, PriorityChild, PriorityLoadBalancingConfig } from "./load-balancer-priority"; -import { WeightedTarget, WeightedTargetLoadBalancingConfig } from "./load-balancer-weighted-target"; +import { LocalitySubchannelAddress, PriorityChildRaw } from "./load-balancer-priority"; import { getSingletonXdsClient, Watcher, XdsClient } from "./xds-client"; -import { DropCategory, XdsClusterImplLoadBalancingConfig } from "./load-balancer-xds-cluster-impl"; +import { DropCategory } from "./load-balancer-xds-cluster-impl"; -import LoadBalancingConfig = experimental.LoadBalancingConfig; -import validateLoadBalancingConfig = experimental.validateLoadBalancingConfig; +import TypedLoadBalancingConfig = experimental.TypedLoadBalancingConfig; import LoadBalancer = experimental.LoadBalancer; import Resolver = experimental.Resolver; import SubchannelAddress = experimental.SubchannelAddress; import ChildLoadBalancerHandler = experimental.ChildLoadBalancerHandler; import createResolver = experimental.createResolver; import ChannelControlHelper = experimental.ChannelControlHelper; -import OutlierDetectionLoadBalancingConfig = experimental.OutlierDetectionLoadBalancingConfig; +import OutlierDetectionRawConfig = experimental.OutlierDetectionRawConfig; import subchannelAddressToString = experimental.subchannelAddressToString; +import selectLbConfigFromList = experimental.selectLbConfigFromList; +import parseLoadBalancingConfig = experimental.parseLoadBalancingConfig; +import UnavailablePicker = experimental.UnavailablePicker; import { serverConfigEqual, validateXdsServerConfig, XdsServerConfig } from "./xds-bootstrap"; import { EndpointResourceType } from "./xds-resource-type/endpoint-resource-type"; +import { WeightedTargetRaw } from "./load-balancer-weighted-target"; const TRACER_NAME = 'xds_cluster_resolver'; @@ -52,7 +53,7 @@ export interface DiscoveryMechanism { type: 'EDS' | 'LOGICAL_DNS'; eds_service_name?: string; dns_hostname?: string; - outlier_detection?: OutlierDetectionLoadBalancingConfig; + outlier_detection?: OutlierDetectionRawConfig; } function validateDiscoveryMechanism(obj: any): DiscoveryMechanism { @@ -62,41 +63,34 @@ function validateDiscoveryMechanism(obj: any): DiscoveryMechanism { if (!('type' in obj && (obj.type === 'EDS' || obj.type === 'LOGICAL_DNS'))) { throw new Error('discovery_mechanisms entry must have a field "type" with the value "EDS" or "LOGICAL_DNS"'); } - if ('max_concurrent_requests' in obj && typeof obj.max_concurrent_requests !== "number") { + if ('max_concurrent_requests' in obj && obj.max_concurrent_requests !== undefined && typeof obj.max_concurrent_requests !== "number") { throw new Error('discovery_mechanisms entry max_concurrent_requests field must be a number if provided'); } - if ('eds_service_name' in obj && typeof obj.eds_service_name !== 'string') { + if ('eds_service_name' in obj && obj.eds_service_name !== undefined && typeof obj.eds_service_name !== 'string') { throw new Error('discovery_mechanisms entry eds_service_name field must be a string if provided'); } - if ('dns_hostname' in obj && typeof obj.dns_hostname !== 'string') { + if ('dns_hostname' in obj && obj.dns_hostname !== undefined && typeof obj.dns_hostname !== 'string') { throw new Error('discovery_mechanisms entry dns_hostname field must be a string if provided'); } - if (EXPERIMENTAL_OUTLIER_DETECTION) { - 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'); - } - return {...obj, lrs_load_reporting_server: validateXdsServerConfig(obj.lrs_load_reporting_server), outlier_detection: outlierDetectionConfig}; - } - return obj; + return {...obj, lrs_load_reporting_server: obj.lrs_load_reporting_server ? validateXdsServerConfig(obj.lrs_load_reporting_server) : undefined}; } const TYPE_NAME = 'xds_cluster_resolver'; -export class XdsClusterResolverLoadBalancingConfig implements LoadBalancingConfig { +class XdsClusterResolverLoadBalancingConfig implements TypedLoadBalancingConfig { getLoadBalancerName(): string { return TYPE_NAME; } toJsonObject(): object { return { [TYPE_NAME]: { - discovery_mechanisms: this.discoveryMechanisms.map(mechanism => ({...mechanism, outlier_detection: mechanism.outlier_detection?.toJsonObject()})), - locality_picking_policy: this.localityPickingPolicy.map(policy => policy.toJsonObject()), - endpoint_picking_policy: this.endpointPickingPolicy.map(policy => policy.toJsonObject()) + discovery_mechanisms: this.discoveryMechanisms, + locality_picking_policy: this.localityPickingPolicy, + endpoint_picking_policy: this.endpointPickingPolicy } } } - + constructor(private discoveryMechanisms: DiscoveryMechanism[], private localityPickingPolicy: LoadBalancingConfig[], private endpointPickingPolicy: LoadBalancingConfig[]) {} getDiscoveryMechanisms() { @@ -123,8 +117,8 @@ export class XdsClusterResolverLoadBalancingConfig implements LoadBalancingConfi } return new XdsClusterResolverLoadBalancingConfig( obj.discovery_mechanisms.map(validateDiscoveryMechanism), - obj.locality_picking_policy.map(validateLoadBalancingConfig), - obj.endpoint_picking_policy.map(validateLoadBalancingConfig) + obj.locality_picking_policy, + obj.endpoint_picking_policy ); } } @@ -264,16 +258,15 @@ export class XdsClusterResolver implements LoadBalancer { } } const fullPriorityList: string[] = []; - const priorityChildren = new Map(); + const priorityChildren: {[name: string]: PriorityChildRaw} = {}; const addressList: LocalitySubchannelAddress[] = []; for (const entry of this.discoveryMechanismList) { const newPriorityNames: string[] = []; const newLocalityPriorities = new Map(); - const defaultEndpointPickingPolicy = entry.discoveryMechanism.type === 'EDS' ? validateLoadBalancingConfig({ round_robin: {} }) : validateLoadBalancingConfig({ pick_first: {} }); - const endpointPickingPolicy: LoadBalancingConfig[] = [ - ...this.latestConfig.getEndpointPickingPolicy(), - defaultEndpointPickingPolicy - ]; + const configEndpointPickingPolicy = this.latestConfig.getEndpointPickingPolicy(); + const defaultEndpointPickingPolicy: LoadBalancingConfig = entry.discoveryMechanism.type === 'EDS' ? { round_robin: {} } : { pick_first: {} }; + const endpointPickingPolicy: LoadBalancingConfig[] = configEndpointPickingPolicy.length > 0 ? configEndpointPickingPolicy : [defaultEndpointPickingPolicy]; + for (const [priority, priorityEntry] of entry.latestUpdate!.entries()) { /** * Highest (smallest number) priority value that any of the localities in @@ -308,18 +301,25 @@ export class XdsClusterResolver implements LoadBalancer { } newPriorityNames[priority] = newPriorityName; - const childTargets = new Map(); + const childTargets: {[locality: string]: WeightedTargetRaw} = {}; for (const localityObj of priorityEntry.localities) { let childPolicy: LoadBalancingConfig[]; if (entry.discoveryMechanism.lrs_load_reporting_server !== undefined) { - childPolicy = [new LrsLoadBalancingConfig(entry.discoveryMechanism.cluster, entry.discoveryMechanism.eds_service_name ?? '', entry.discoveryMechanism.lrs_load_reporting_server, localityObj.locality, endpointPickingPolicy)]; + childPolicy = [{ + lrs: { + cluster_name: entry.discoveryMechanism.cluster, + eds_service_name: entry.discoveryMechanism.eds_service_name ?? '', + locality: {...localityObj.locality}, + lrs_load_reporting_server: {...entry.discoveryMechanism.lrs_load_reporting_server} + } + }]; } else { childPolicy = endpointPickingPolicy; } - childTargets.set(localityToName(localityObj.locality), { + childTargets[localityToName(localityObj.locality)] = { weight: localityObj.weight, child_policy: childPolicy, - }); + }; for (const address of localityObj.addresses) { addressList.push({ localityPath: [ @@ -331,34 +331,65 @@ export class XdsClusterResolver implements LoadBalancer { } newLocalityPriorities.set(localityToName(localityObj.locality), priority); } - const weightedTargetConfig = new WeightedTargetLoadBalancingConfig(childTargets); - const xdsClusterImplConfig = new XdsClusterImplLoadBalancingConfig(entry.discoveryMechanism.cluster, priorityEntry.dropCategories, [weightedTargetConfig], entry.discoveryMechanism.eds_service_name, entry.discoveryMechanism.lrs_load_reporting_server, entry.discoveryMechanism.max_concurrent_requests); - let outlierDetectionConfig: OutlierDetectionLoadBalancingConfig | undefined; + const xdsClusterImplConfig = { + xds_cluster_impl: { + cluster: entry.discoveryMechanism.cluster, + drop_categories: priorityEntry.dropCategories, + max_concurrent_requests: entry.discoveryMechanism.max_concurrent_requests, + eds_service_name: entry.discoveryMechanism.eds_service_name, + lrs_load_reporting_server: entry.discoveryMechanism.lrs_load_reporting_server, + child_policy: [{ + weighted_target: { + targets: childTargets + } + }] + } + } + let priorityChildConfig: LoadBalancingConfig; if (EXPERIMENTAL_OUTLIER_DETECTION) { - outlierDetectionConfig = entry.discoveryMechanism.outlier_detection?.copyWithChildPolicy([xdsClusterImplConfig]); + priorityChildConfig = { + outlier_detection: { + ...entry.discoveryMechanism.outlier_detection, + child_policy: [xdsClusterImplConfig] + } + } + } else { + priorityChildConfig = xdsClusterImplConfig; } - const priorityChildConfig = outlierDetectionConfig ?? xdsClusterImplConfig; - - priorityChildren.set(newPriorityName, { + + priorityChildren[newPriorityName] = { config: [priorityChildConfig], ignore_reresolution_requests: entry.discoveryMechanism.type === 'EDS' - }); + }; } entry.localityPriorities = newLocalityPriorities; entry.priorityNames = newPriorityNames; fullPriorityList.push(...newPriorityNames); } - const childConfig: PriorityLoadBalancingConfig = new PriorityLoadBalancingConfig(priorityChildren, fullPriorityList); + const childConfig = { + priority: { + children: priorityChildren, + priorities: fullPriorityList + } + } + let typedChildConfig: TypedLoadBalancingConfig; + try { + typedChildConfig = parseLoadBalancingConfig(childConfig); + } catch (e) { + trace('LB policy config parsing failed with error ' + (e as Error).message); + this.channelControlHelper.updateState(connectivityState.TRANSIENT_FAILURE, new UnavailablePicker({code: status.UNAVAILABLE, details: `LB policy config parsing failed with error ${(e as Error).message}`, metadata: new Metadata()})); + return; + } trace('Child update addresses: ' + addressList.map(address => '(' + subchannelAddressToString(address) + ' path=' + address.localityPath + ')')); - trace('Child update priority config: ' + JSON.stringify(childConfig.toJsonObject(), undefined, 2)); + trace('Child update priority config: ' + JSON.stringify(childConfig, undefined, 2)); this.childBalancer.updateAddressList( addressList, - childConfig, + typedChildConfig, this.latestAttributes ); } - updateAddressList(addressList: SubchannelAddress[], lbConfig: LoadBalancingConfig, attributes: { [key: string]: unknown; }): void { + updateAddressList(addressList: SubchannelAddress[], lbConfig: TypedLoadBalancingConfig, attributes: { [key: string]: unknown; }): void { if (!(lbConfig instanceof XdsClusterResolverLoadBalancingConfig)) { trace('Discarding address list update with unrecognized config ' + JSON.stringify(lbConfig, undefined, 2)); return; @@ -459,7 +490,7 @@ function maybeServerConfigEqual(config1: XdsServerConfig | undefined, config2: X } export class XdsClusterResolverChildPolicyHandler extends ChildLoadBalancerHandler { - protected configUpdateRequiresNewPolicyInstance(oldConfig: LoadBalancingConfig, newConfig: LoadBalancingConfig): boolean { + protected configUpdateRequiresNewPolicyInstance(oldConfig: TypedLoadBalancingConfig, newConfig: TypedLoadBalancingConfig): boolean { if (!(oldConfig instanceof XdsClusterResolverLoadBalancingConfig && newConfig instanceof XdsClusterResolverLoadBalancingConfig)) { return super.configUpdateRequiresNewPolicyInstance(oldConfig, newConfig); } diff --git a/packages/grpc-js-xds/src/resolver-xds.ts b/packages/grpc-js-xds/src/resolver-xds.ts index a934e7285..cd830d521 100644 --- a/packages/grpc-js-xds/src/resolver-xds.ts +++ b/packages/grpc-js-xds/src/resolver-xds.ts @@ -19,23 +19,19 @@ import * as protoLoader from '@grpc/proto-loader'; import { RE2 } from 're2-wasm'; import { getSingletonXdsClient, Watcher, XdsClient } from './xds-client'; -import { StatusObject, status, logVerbosity, Metadata, experimental, ChannelOptions } from '@grpc/grpc-js'; +import { StatusObject, status, logVerbosity, Metadata, experimental, ChannelOptions, ServiceConfig, LoadBalancingConfig, RetryPolicy } from '@grpc/grpc-js'; import Resolver = experimental.Resolver; import GrpcUri = experimental.GrpcUri; import ResolverListener = experimental.ResolverListener; import uriToString = experimental.uriToString; -import ServiceConfig = experimental.ServiceConfig; import registerResolver = experimental.registerResolver; import { Listener__Output } from './generated/envoy/config/listener/v3/Listener'; import { RouteConfiguration__Output } from './generated/envoy/config/route/v3/RouteConfiguration'; import { HttpConnectionManager__Output } from './generated/envoy/extensions/filters/network/http_connection_manager/v3/HttpConnectionManager'; -import { CdsLoadBalancingConfig } from './load-balancer-cds'; import { VirtualHost__Output } from './generated/envoy/config/route/v3/VirtualHost'; import { RouteMatch__Output } from './generated/envoy/config/route/v3/RouteMatch'; import { HeaderMatcher__Output } from './generated/envoy/config/route/v3/HeaderMatcher'; import ConfigSelector = experimental.ConfigSelector; -import LoadBalancingConfig = experimental.LoadBalancingConfig; -import { XdsClusterManagerLoadBalancingConfig } from './load-balancer-xds-cluster-manager'; import { ContainsValueMatcher, ExactValueMatcher, FullMatcher, HeaderMatcher, Matcher, PathExactValueMatcher, PathPrefixValueMatcher, PathSafeRegexValueMatcher, PrefixValueMatcher, PresentValueMatcher, RangeValueMatcher, RejectValueMatcher, SafeRegexValueMatcher, SuffixValueMatcher, ValueMatcher } from './matcher'; import { envoyFractionToFraction, Fraction } from "./fraction"; import { RouteAction, SingleClusterRouteAction, WeightedCluster, WeightedClusterRouteAction } from './route-action'; @@ -46,10 +42,10 @@ import { createHttpFilter, HttpFilterConfig, parseOverrideFilterConfig, parseTop import { EXPERIMENTAL_FAULT_INJECTION, EXPERIMENTAL_FEDERATION, EXPERIMENTAL_RETRY } from './environment'; import Filter = experimental.Filter; import FilterFactory = experimental.FilterFactory; -import RetryPolicy = experimental.RetryPolicy; import { BootstrapInfo, loadBootstrapInfo, validateBootstrapConfig } from './xds-bootstrap'; import { ListenerResourceType } from './xds-resource-type/listener-resource-type'; import { RouteConfigurationResourceType } from './xds-resource-type/route-config-resource-type'; +import { protoDurationToDuration } from './duration'; const TRACER_NAME = 'xds_resolver'; @@ -208,20 +204,6 @@ function getPredicateForMatcher(routeMatch: RouteMatch__Output): Matcher { return new FullMatcher(pathMatcher, headerMatchers, runtimeFraction); } -/** - * Convert a Duration protobuf message object to a Duration object as used in - * the ServiceConfig definition. The difference is that the protobuf message - * defines seconds as a long, which is represented as a string in JavaScript, - * and the one used in the service config defines it as a number. - * @param duration - */ -function protoDurationToDuration(duration: Duration__Output): Duration { - return { - seconds: Number.parseInt(duration.seconds), - nanos: duration.nanos - } -} - function protoDurationToSecondsString(duration: Duration__Output): string { return `${duration.seconds + duration.nanos / 1_000_000_000}s`; } @@ -235,7 +217,7 @@ function getDefaultRetryMaxInterval(baseInterval: string): string { /** * Encode a text string as a valid path of a URI, as specified in RFC-3986 section 3.3 * @param uriPath A value representing an unencoded URI path - * @returns + * @returns */ function encodeURIPath(uriPath: string): string { return uriPath.replace(/[^A-Za-z0-9._~!$&^()*+,;=/-]/g, substring => encodeURIComponent(substring)); @@ -447,7 +429,7 @@ class XdsResolver implements Resolver { } } } - let retryPolicy: RetryPolicy | undefined = undefined; + let retryPolicy: RetryPolicy | undefined = undefined; if (EXPERIMENTAL_RETRY) { const retryConfig = route.route!.retry_policy ?? virtualHost.retry_policy; if (retryConfig) { @@ -458,10 +440,10 @@ class XdsResolver implements Resolver { } } if (retryableStatusCodes.length > 0) { - const baseInterval = retryConfig.retry_back_off?.base_interval ? - protoDurationToSecondsString(retryConfig.retry_back_off.base_interval) : + const baseInterval = retryConfig.retry_back_off?.base_interval ? + protoDurationToSecondsString(retryConfig.retry_back_off.base_interval) : DEFAULT_RETRY_BASE_INTERVAL; - const maxInterval = retryConfig.retry_back_off?.max_interval ? + const maxInterval = retryConfig.retry_back_off?.max_interval ? protoDurationToSecondsString(retryConfig.retry_back_off.max_interval) : getDefaultRetryMaxInterval(baseInterval); retryPolicy = { @@ -602,11 +584,11 @@ class XdsResolver implements Resolver { trace(matcher.toString()); trace('=> ' + action.toString()); } - const clusterConfigMap = new Map(); + const clusterConfigMap: {[key: string]: {child_policy: LoadBalancingConfig[]}} = {}; for (const clusterName of this.clusterRefcounts.keys()) { - clusterConfigMap.set(clusterName, {child_policy: [new CdsLoadBalancingConfig(clusterName)]}); + clusterConfigMap[clusterName] = {child_policy: [{cds: {cluster: clusterName}}]}; } - const lbPolicyConfig = new XdsClusterManagerLoadBalancingConfig(clusterConfigMap); + const lbPolicyConfig = {xds_cluster_manager: {children: clusterConfigMap}}; const serviceConfig: ServiceConfig = { methodConfig: [], loadBalancingConfig: [lbPolicyConfig] @@ -634,7 +616,7 @@ class XdsResolver implements Resolver { this.isLdsWatcherActive = true; } catch (e) { - this.reportResolutionError(e.message); + this.reportResolutionError((e as Error).message); } } } @@ -647,7 +629,7 @@ class XdsResolver implements Resolver { try { this.bootstrapInfo = loadBootstrapInfo(); } catch (e) { - this.reportResolutionError(e.message); + this.reportResolutionError((e as Error).message); } this.startResolution(); } diff --git a/packages/grpc-js-xds/src/route-action.ts b/packages/grpc-js-xds/src/route-action.ts index 5ae5885af..83530f1b4 100644 --- a/packages/grpc-js-xds/src/route-action.ts +++ b/packages/grpc-js-xds/src/route-action.ts @@ -14,11 +14,10 @@ * limitations under the License. */ -import { experimental } from '@grpc/grpc-js'; +import { MethodConfig, experimental } from '@grpc/grpc-js'; import Duration = experimental.Duration; import Filter = experimental.Filter; import FilterFactory = experimental.FilterFactory; -import MethodConfig = experimental.MethodConfig; export interface ClusterResult { name: string; @@ -101,4 +100,4 @@ export class WeightedClusterRouteAction implements RouteAction { const clusterListString = this.clusters.map(({name, weight}) => '(' + name + ':' + weight + ')').join(', ') return 'WeightedCluster(' + clusterListString + ', ' + JSON.stringify(this.methodConfig) + ')'; } -} \ No newline at end of file +} diff --git a/packages/grpc-js-xds/src/xds-resource-type/cluster-resource-type.ts b/packages/grpc-js-xds/src/xds-resource-type/cluster-resource-type.ts index f431bf238..e617bef94 100644 --- a/packages/grpc-js-xds/src/xds-resource-type/cluster-resource-type.ts +++ b/packages/grpc-js-xds/src/xds-resource-type/cluster-resource-type.ts @@ -29,15 +29,7 @@ import { Any__Output } from "../generated/google/protobuf/Any"; import SuccessRateEjectionConfig = experimental.SuccessRateEjectionConfig; import FailurePercentageEjectionConfig = experimental.FailurePercentageEjectionConfig; import { Watcher, XdsClient } from "../xds-client"; - -export interface OutlierDetectionUpdate { - intervalMs: number | null; - baseEjectionTimeMs: number | null; - maxEjectionTimeMs: number | null; - maxEjectionPercent: number | null; - successRateConfig: Partial | null; - failurePercentageConfig: Partial | null; -} +import { protoDurationToDuration } from "../duration"; export interface CdsUpdate { type: 'AGGREGATE' | 'EDS' | 'LOGICAL_DNS'; @@ -47,29 +39,20 @@ export interface CdsUpdate { maxConcurrentRequests?: number; edsServiceName?: string; dnsHostname?: string; - outlierDetectionUpdate?: OutlierDetectionUpdate; + outlierDetectionUpdate?: experimental.OutlierDetectionRawConfig; } -function durationToMs(duration: Duration__Output): number { - return (Number(duration.seconds) * 1_000 + duration.nanos / 1_000_000) | 0; -} - -function convertOutlierDetectionUpdate(outlierDetection: OutlierDetection__Output | null): OutlierDetectionUpdate | undefined { +function convertOutlierDetectionUpdate(outlierDetection: OutlierDetection__Output | null): experimental.OutlierDetectionRawConfig | undefined { if (!EXPERIMENTAL_OUTLIER_DETECTION) { return undefined; } if (!outlierDetection) { /* No-op outlier detection config, with all fields unset. */ return { - intervalMs: null, - baseEjectionTimeMs: null, - maxEjectionTimeMs: null, - maxEjectionPercent: null, - successRateConfig: null, - failurePercentageConfig: null + child_policy: [] }; } - let successRateConfig: Partial | null = null; + let successRateConfig: Partial | undefined = undefined; /* Success rate ejection is enabled by default, so we only disable it if * enforcing_success_rate is set and it has the value 0 */ if (!outlierDetection.enforcing_success_rate || outlierDetection.enforcing_success_rate.value > 0) { @@ -80,7 +63,7 @@ function convertOutlierDetectionUpdate(outlierDetection: OutlierDetection__Outpu stdev_factor: outlierDetection.success_rate_stdev_factor?.value }; } - let failurePercentageConfig: Partial | null = null; + let failurePercentageConfig: Partial | undefined = undefined; /* Failure percentage ejection is disabled by default, so we only enable it * if enforcing_failure_percentage is set and it has a value greater than 0 */ if (outlierDetection.enforcing_failure_percentage && outlierDetection.enforcing_failure_percentage.value > 0) { @@ -92,19 +75,20 @@ function convertOutlierDetectionUpdate(outlierDetection: OutlierDetection__Outpu } } return { - intervalMs: outlierDetection.interval ? durationToMs(outlierDetection.interval) : null, - baseEjectionTimeMs: outlierDetection.base_ejection_time ? durationToMs(outlierDetection.base_ejection_time) : null, - maxEjectionTimeMs: outlierDetection.max_ejection_time ? durationToMs(outlierDetection.max_ejection_time) : null, - maxEjectionPercent : outlierDetection.max_ejection_percent?.value ?? null, - successRateConfig: successRateConfig, - failurePercentageConfig: failurePercentageConfig + interval: outlierDetection.interval ? protoDurationToDuration(outlierDetection.interval) : undefined, + base_ejection_time: outlierDetection.base_ejection_time ? protoDurationToDuration(outlierDetection.base_ejection_time) : undefined, + max_ejection_time: outlierDetection.max_ejection_time ? protoDurationToDuration(outlierDetection.max_ejection_time) : undefined, + max_ejection_percent: outlierDetection.max_ejection_percent?.value, + success_rate_ejection: successRateConfig, + failure_percentage_ejection: failurePercentageConfig, + child_policy: [] }; } export class ClusterResourceType extends XdsResourceType { private static singleton: ClusterResourceType = new ClusterResourceType(); - + private constructor() { super(); } @@ -124,7 +108,7 @@ export class ClusterResourceType extends XdsResourceType { /* The maximum values here come from the official Protobuf documentation: * https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#google.protobuf.Duration */ - return Number(duration.seconds) >= 0 && + return Number(duration.seconds) >= 0 && Number(duration.seconds) <= 315_576_000_000 && duration.nanos >= 0 && duration.nanos <= 999_999_999; diff --git a/packages/grpc-js/src/experimental.ts b/packages/grpc-js/src/experimental.ts index 9e4bbf45b..e4bd164ec 100644 --- a/packages/grpc-js/src/experimental.ts +++ b/packages/grpc-js/src/experimental.ts @@ -8,16 +8,15 @@ export { } from './resolver'; export { GrpcUri, uriToString } from './uri-parser'; export { Duration, durationToMs } from './duration'; -export { ServiceConfig, MethodConfig, RetryPolicy } from './service-config'; export { BackoffTimeout } from './backoff-timeout'; export { LoadBalancer, - LoadBalancingConfig, + TypedLoadBalancingConfig, ChannelControlHelper, createChildChannelControlHelper, registerLoadBalancerType, - getFirstUsableConfig, - validateLoadBalancingConfig, + selectLbConfigFromList, + parseLoadBalancingConfig } from './load-balancer'; export { SubchannelAddress, @@ -42,7 +41,7 @@ export { ConnectivityStateListener, } from './subchannel-interface'; export { - OutlierDetectionLoadBalancingConfig, + OutlierDetectionRawConfig, SuccessRateEjectionConfig, FailurePercentageEjectionConfig, } from './load-balancer-outlier-detection'; diff --git a/packages/grpc-js/src/index.ts b/packages/grpc-js/src/index.ts index adacae08f..d44a2dc6e 100644 --- a/packages/grpc-js/src/index.ts +++ b/packages/grpc-js/src/index.ts @@ -261,6 +261,8 @@ export { getChannelzServiceDefinition, getChannelzHandlers } from './channelz'; export { addAdminServicesToServer } from './admin'; +export { ServiceConfig, LoadBalancingConfig, MethodConfig, RetryPolicy } from './service-config'; + import * as experimental from './experimental'; export { experimental }; diff --git a/packages/grpc-js/src/load-balancer-child-handler.ts b/packages/grpc-js/src/load-balancer-child-handler.ts index a4dc90c4f..b23f19263 100644 --- a/packages/grpc-js/src/load-balancer-child-handler.ts +++ b/packages/grpc-js/src/load-balancer-child-handler.ts @@ -18,7 +18,7 @@ import { LoadBalancer, ChannelControlHelper, - LoadBalancingConfig, + TypedLoadBalancingConfig, createLoadBalancer, } from './load-balancer'; import { SubchannelAddress } from './subchannel-address'; @@ -33,7 +33,7 @@ const TYPE_NAME = 'child_load_balancer_helper'; export class ChildLoadBalancerHandler implements LoadBalancer { private currentChild: LoadBalancer | null = null; private pendingChild: LoadBalancer | null = null; - private latestConfig: LoadBalancingConfig | null = null; + private latestConfig: TypedLoadBalancingConfig | null = null; private ChildPolicyHelper = class { private child: LoadBalancer | null = null; @@ -87,8 +87,8 @@ export class ChildLoadBalancerHandler implements LoadBalancer { constructor(private readonly channelControlHelper: ChannelControlHelper) {} protected configUpdateRequiresNewPolicyInstance( - oldConfig: LoadBalancingConfig, - newConfig: LoadBalancingConfig + oldConfig: TypedLoadBalancingConfig, + newConfig: TypedLoadBalancingConfig ): boolean { return oldConfig.getLoadBalancerName() !== newConfig.getLoadBalancerName(); } @@ -101,7 +101,7 @@ export class ChildLoadBalancerHandler implements LoadBalancer { */ updateAddressList( addressList: SubchannelAddress[], - lbConfig: LoadBalancingConfig, + lbConfig: TypedLoadBalancingConfig, attributes: { [key: string]: unknown } ): void { let childToUpdate: LoadBalancer; diff --git a/packages/grpc-js/src/load-balancer-outlier-detection.ts b/packages/grpc-js/src/load-balancer-outlier-detection.ts index 4abbd0843..00d0e0a53 100644 --- a/packages/grpc-js/src/load-balancer-outlier-detection.ts +++ b/packages/grpc-js/src/load-balancer-outlier-detection.ts @@ -18,17 +18,16 @@ import { ChannelOptions } from './channel-options'; import { ConnectivityState } from './connectivity-state'; import { LogVerbosity, Status } from './constants'; -import { durationToMs, isDuration, msToDuration } from './duration'; +import { Duration, durationToMs, isDuration, msToDuration } from './duration'; import { ChannelControlHelper, createChildChannelControlHelper, registerLoadBalancerType, } from './experimental'; import { - getFirstUsableConfig, + selectLbConfigFromList, LoadBalancer, - LoadBalancingConfig, - validateLoadBalancingConfig, + TypedLoadBalancingConfig, } from './load-balancer'; import { ChildLoadBalancerHandler } from './load-balancer-child-handler'; import { PickArgs, Picker, PickResult, PickResultType } from './picker'; @@ -42,6 +41,7 @@ import { SubchannelInterface, } from './subchannel-interface'; import * as logging from './logging'; +import { LoadBalancingConfig } from './service-config'; const TRACER_NAME = 'outlier_detection'; @@ -68,6 +68,16 @@ export interface FailurePercentageEjectionConfig { readonly request_volume: number; } +export interface OutlierDetectionRawConfig { + interval?: Duration; + base_ejection_time?: Duration; + max_ejection_time?: Duration; + max_ejection_percent?: number; + success_rate_ejection?: Partial; + failure_percentage_ejection?: Partial; + child_policy: LoadBalancingConfig[]; +} + const defaultSuccessRateEjectionConfig: SuccessRateEjectionConfig = { stdev_factor: 1900, enforcement_percentage: 100, @@ -147,7 +157,7 @@ function validatePercentage(obj: any, fieldName: string, objectName?: string) { } export class OutlierDetectionLoadBalancingConfig - implements LoadBalancingConfig + implements TypedLoadBalancingConfig { private readonly intervalMs: number; private readonly baseEjectionTimeMs: number; @@ -163,11 +173,10 @@ export class OutlierDetectionLoadBalancingConfig maxEjectionPercent: number | null, successRateEjection: Partial | null, failurePercentageEjection: Partial | null, - private readonly childPolicy: LoadBalancingConfig[] + private readonly childPolicy: TypedLoadBalancingConfig ) { if ( - childPolicy.length > 0 && - childPolicy[0].getLoadBalancerName() === 'pick_first' + childPolicy.getLoadBalancerName() === 'pick_first' ) { throw new Error( 'outlier_detection LB policy cannot have a pick_first child policy' @@ -198,7 +207,7 @@ export class OutlierDetectionLoadBalancingConfig max_ejection_percent: this.maxEjectionPercent, success_rate_ejection: this.successRateEjection, failure_percentage_ejection: this.failurePercentageEjection, - child_policy: this.childPolicy.map(policy => policy.toJsonObject()), + child_policy: [this.childPolicy.toJsonObject()] }; } @@ -220,24 +229,10 @@ export class OutlierDetectionLoadBalancingConfig getFailurePercentageEjectionConfig(): FailurePercentageEjectionConfig | null { return this.failurePercentageEjection; } - getChildPolicy(): LoadBalancingConfig[] { + getChildPolicy(): TypedLoadBalancingConfig { return this.childPolicy; } - copyWithChildPolicy( - childPolicy: LoadBalancingConfig[] - ): OutlierDetectionLoadBalancingConfig { - return new OutlierDetectionLoadBalancingConfig( - this.intervalMs, - this.baseEjectionTimeMs, - this.maxEjectionTimeMs, - this.maxEjectionPercent, - this.successRateEjection, - this.failurePercentageEjection, - childPolicy - ); - } - static createFromJson(obj: any): OutlierDetectionLoadBalancingConfig { validatePositiveDuration(obj, 'interval'); validatePositiveDuration(obj, 'base_ejection_time'); @@ -303,6 +298,14 @@ export class OutlierDetectionLoadBalancingConfig ); } + if (!('child_policy' in obj) || !Array.isArray(obj.child_policy)) { + throw new Error('outlier detection config child_policy must be an array'); + } + const childPolicy = selectLbConfigFromList(obj.child_policy); + if (!childPolicy) { + throw new Error('outlier detection config child_policy: no valid recognized policy found'); + } + return new OutlierDetectionLoadBalancingConfig( obj.interval ? durationToMs(obj.interval) : null, obj.base_ejection_time ? durationToMs(obj.base_ejection_time) : null, @@ -310,7 +313,7 @@ export class OutlierDetectionLoadBalancingConfig obj.max_ejection_percent ?? null, obj.success_rate_ejection, obj.failure_percentage_ejection, - obj.child_policy.map(validateLoadBalancingConfig) + childPolicy ); } } @@ -794,7 +797,7 @@ export class OutlierDetectionLoadBalancer implements LoadBalancer { updateAddressList( addressList: SubchannelAddress[], - lbConfig: LoadBalancingConfig, + lbConfig: TypedLoadBalancingConfig, attributes: { [key: string]: unknown } ): void { if (!(lbConfig instanceof OutlierDetectionLoadBalancingConfig)) { @@ -821,10 +824,7 @@ export class OutlierDetectionLoadBalancer implements LoadBalancer { this.addressMap.delete(key); } } - const childPolicy: LoadBalancingConfig = getFirstUsableConfig( - lbConfig.getChildPolicy(), - true - ); + const childPolicy = lbConfig.getChildPolicy(); this.childBalancer.updateAddressList(addressList, childPolicy, attributes); if ( diff --git a/packages/grpc-js/src/load-balancer-pick-first.ts b/packages/grpc-js/src/load-balancer-pick-first.ts index 08971980b..8635482ce 100644 --- a/packages/grpc-js/src/load-balancer-pick-first.ts +++ b/packages/grpc-js/src/load-balancer-pick-first.ts @@ -18,7 +18,7 @@ import { LoadBalancer, ChannelControlHelper, - LoadBalancingConfig, + TypedLoadBalancingConfig, registerDefaultLoadBalancerType, registerLoadBalancerType, } from './load-balancer'; @@ -53,7 +53,7 @@ const TYPE_NAME = 'pick_first'; */ const CONNECTION_DELAY_INTERVAL_MS = 250; -export class PickFirstLoadBalancingConfig implements LoadBalancingConfig { +export class PickFirstLoadBalancingConfig implements TypedLoadBalancingConfig { constructor(private readonly shuffleAddressList: boolean) {} getLoadBalancerName(): string { @@ -374,7 +374,7 @@ export class PickFirstLoadBalancer implements LoadBalancer { updateAddressList( addressList: SubchannelAddress[], - lbConfig: LoadBalancingConfig + lbConfig: TypedLoadBalancingConfig ): void { if (!(lbConfig instanceof PickFirstLoadBalancingConfig)) { return; diff --git a/packages/grpc-js/src/load-balancer-round-robin.ts b/packages/grpc-js/src/load-balancer-round-robin.ts index f389fefc0..a611cfd64 100644 --- a/packages/grpc-js/src/load-balancer-round-robin.ts +++ b/packages/grpc-js/src/load-balancer-round-robin.ts @@ -18,7 +18,7 @@ import { LoadBalancer, ChannelControlHelper, - LoadBalancingConfig, + TypedLoadBalancingConfig, registerLoadBalancerType, } from './load-balancer'; import { ConnectivityState } from './connectivity-state'; @@ -49,7 +49,7 @@ function trace(text: string): void { const TYPE_NAME = 'round_robin'; -class RoundRobinLoadBalancingConfig implements LoadBalancingConfig { +class RoundRobinLoadBalancingConfig implements TypedLoadBalancingConfig { getLoadBalancerName(): string { return TYPE_NAME; } @@ -192,7 +192,7 @@ export class RoundRobinLoadBalancer implements LoadBalancer { updateAddressList( addressList: SubchannelAddress[], - lbConfig: LoadBalancingConfig + lbConfig: TypedLoadBalancingConfig ): void { this.resetSubchannelList(); trace( diff --git a/packages/grpc-js/src/load-balancer.ts b/packages/grpc-js/src/load-balancer.ts index f18638788..d5d69543f 100644 --- a/packages/grpc-js/src/load-balancer.ts +++ b/packages/grpc-js/src/load-balancer.ts @@ -21,6 +21,9 @@ import { ConnectivityState } from './connectivity-state'; import { Picker } from './picker'; import { ChannelRef, SubchannelRef } from './channelz'; import { SubchannelInterface } from './subchannel-interface'; +import { LoadBalancingConfig } from './service-config'; +import { log } from './logging'; +import { LogVerbosity } from './constants'; /** * A collection of functions associated with a channel that a load balancer @@ -98,7 +101,7 @@ export interface LoadBalancer { */ updateAddressList( addressList: SubchannelAddress[], - lbConfig: LoadBalancingConfig, + lbConfig: TypedLoadBalancingConfig, attributes: { [key: string]: unknown } ): void; /** @@ -128,22 +131,22 @@ export interface LoadBalancerConstructor { new (channelControlHelper: ChannelControlHelper): LoadBalancer; } -export interface LoadBalancingConfig { +export interface TypedLoadBalancingConfig { getLoadBalancerName(): string; toJsonObject(): object; } -export interface LoadBalancingConfigConstructor { +export interface TypedLoadBalancingConfigConstructor { // eslint-disable-next-line @typescript-eslint/no-explicit-any - new (...args: any): LoadBalancingConfig; + new (...args: any): TypedLoadBalancingConfig; // eslint-disable-next-line @typescript-eslint/no-explicit-any - createFromJson(obj: any): LoadBalancingConfig; + createFromJson(obj: any): TypedLoadBalancingConfig; } const registeredLoadBalancerTypes: { [name: string]: { LoadBalancer: LoadBalancerConstructor; - LoadBalancingConfig: LoadBalancingConfigConstructor; + LoadBalancingConfig: TypedLoadBalancingConfigConstructor; }; } = {}; @@ -152,7 +155,7 @@ let defaultLoadBalancerType: string | null = null; export function registerLoadBalancerType( typeName: string, loadBalancerType: LoadBalancerConstructor, - loadBalancingConfigType: LoadBalancingConfigConstructor + loadBalancingConfigType: TypedLoadBalancingConfigConstructor ) { registeredLoadBalancerTypes[typeName] = { LoadBalancer: loadBalancerType, @@ -165,7 +168,7 @@ export function registerDefaultLoadBalancerType(typeName: string) { } export function createLoadBalancer( - config: LoadBalancingConfig, + config: TypedLoadBalancingConfig, channelControlHelper: ChannelControlHelper ): LoadBalancer | null { const typeName = config.getLoadBalancerName(); @@ -182,17 +185,44 @@ export function isLoadBalancerNameRegistered(typeName: string): boolean { return typeName in registeredLoadBalancerTypes; } -export function getFirstUsableConfig( - configs: LoadBalancingConfig[], - fallbackTodefault?: true -): LoadBalancingConfig; -export function getFirstUsableConfig( +export function parseLoadBalancingConfig(rawConfig: LoadBalancingConfig): TypedLoadBalancingConfig { + const keys = Object.keys(rawConfig); + if (keys.length !== 1) { + throw new Error( + 'Provided load balancing config has multiple conflicting entries' + ); + } + const typeName = keys[0]; + if (typeName in registeredLoadBalancerTypes) { + try { + return registeredLoadBalancerTypes[ + typeName + ].LoadBalancingConfig.createFromJson(rawConfig[typeName]); + } catch (e) { + throw new Error(`${typeName}: ${(e as Error).message}`); + } + } else { + throw new Error(`Unrecognized load balancing config name ${typeName}`); + } +} + +export function getDefaultConfig() { + if (!defaultLoadBalancerType) { + throw new Error('No default load balancer type registered'); + } + return new registeredLoadBalancerTypes[defaultLoadBalancerType]!.LoadBalancingConfig(); +} + +export function selectLbConfigFromList( configs: LoadBalancingConfig[], fallbackTodefault = false -): LoadBalancingConfig | null { +): TypedLoadBalancingConfig | null { for (const config of configs) { - if (config.getLoadBalancerName() in registeredLoadBalancerTypes) { - return config; + try { + return parseLoadBalancingConfig(config); + } catch (e) { + log(LogVerbosity.DEBUG, 'Config parsing failed with error', (e as Error).message); + continue; } } if (fallbackTodefault) { @@ -207,24 +237,3 @@ export function getFirstUsableConfig( return null; } } - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function validateLoadBalancingConfig(obj: any): LoadBalancingConfig { - if (!(obj !== null && typeof obj === 'object')) { - throw new Error('Load balancing config must be an object'); - } - const keys = Object.keys(obj); - if (keys.length !== 1) { - throw new Error( - 'Provided load balancing config has multiple conflicting entries' - ); - } - const typeName = keys[0]; - if (typeName in registeredLoadBalancerTypes) { - return registeredLoadBalancerTypes[ - typeName - ].LoadBalancingConfig.createFromJson(obj[typeName]); - } else { - throw new Error(`Unrecognized load balancing config name ${typeName}`); - } -} diff --git a/packages/grpc-js/src/resolving-load-balancer.ts b/packages/grpc-js/src/resolving-load-balancer.ts index d49609ff2..e2b2c1fa5 100644 --- a/packages/grpc-js/src/resolving-load-balancer.ts +++ b/packages/grpc-js/src/resolving-load-balancer.ts @@ -18,8 +18,8 @@ import { ChannelControlHelper, LoadBalancer, - LoadBalancingConfig, - getFirstUsableConfig, + TypedLoadBalancingConfig, + selectLbConfigFromList, } from './load-balancer'; import { ServiceConfig, validateServiceConfig } from './service-config'; import { ConnectivityState } from './connectivity-state'; @@ -211,7 +211,7 @@ export class ResolvingLoadBalancer implements LoadBalancer { } const workingConfigList = workingServiceConfig?.loadBalancingConfig ?? []; - const loadBalancingConfig = getFirstUsableConfig( + const loadBalancingConfig = selectLbConfigFromList( workingConfigList, true ); @@ -308,7 +308,7 @@ export class ResolvingLoadBalancer implements LoadBalancer { updateAddressList( addressList: SubchannelAddress[], - lbConfig: LoadBalancingConfig | null + lbConfig: TypedLoadBalancingConfig | null ): never { throw new Error('updateAddressList not supported on ResolvingLoadBalancer'); } diff --git a/packages/grpc-js/src/service-config.ts b/packages/grpc-js/src/service-config.ts index 91bee52c2..168f28c78 100644 --- a/packages/grpc-js/src/service-config.ts +++ b/packages/grpc-js/src/service-config.ts @@ -29,10 +29,6 @@ import * as os from 'os'; import { Status } from './constants'; import { Duration } from './duration'; -import { - LoadBalancingConfig, - validateLoadBalancingConfig, -} from './load-balancer'; export interface MethodConfigName { service: string; @@ -68,6 +64,10 @@ export interface RetryThrottling { tokenRatio: number; } +export interface LoadBalancingConfig { + [key: string]: object; +} + export interface ServiceConfig { loadBalancingPolicy?: string; loadBalancingConfig: LoadBalancingConfig[]; @@ -338,6 +338,22 @@ export function validateRetryThrottling(obj: any): RetryThrottling { }; } +function validateLoadBalancingConfig(obj: any): LoadBalancingConfig { + if (!(typeof obj === 'object' && obj !== null)) { + throw new Error(`Invalid loadBalancingConfig: unexpected type ${typeof obj}`); + } + const keys = Object.keys(obj); + if (keys.length > 1) { + throw new Error(`Invalid loadBalancingConfig: unexpected multiple keys ${keys}`); + } + if (keys.length === 0) { + throw new Error('Invalid loadBalancingConfig: load balancing policy name required'); + } + return { + [keys[0]]: obj[keys[0]] + }; +} + export function validateServiceConfig(obj: any): ServiceConfig { const result: ServiceConfig = { loadBalancingConfig: [], @@ -353,6 +369,7 @@ export function validateServiceConfig(obj: any): ServiceConfig { if ('loadBalancingConfig' in obj) { if (Array.isArray(obj.loadBalancingConfig)) { for (const config of obj.loadBalancingConfig) { + result.loadBalancingConfig.push(validateLoadBalancingConfig(config)); } } else { diff --git a/packages/grpc-js/test/test-deadline.ts b/packages/grpc-js/test/test-deadline.ts index 9de3687c4..24aebd4d7 100644 --- a/packages/grpc-js/test/test-deadline.ts +++ b/packages/grpc-js/test/test-deadline.ts @@ -18,12 +18,10 @@ import * as assert from 'assert'; import * as grpc from '../src'; -import { experimental } from '../src'; import { ServiceClient, ServiceClientConstructor } from '../src/make-client'; import { loadProtoFile } from './common'; -import ServiceConfig = experimental.ServiceConfig; -const TIMEOUT_SERVICE_CONFIG: ServiceConfig = { +const TIMEOUT_SERVICE_CONFIG: grpc.ServiceConfig = { loadBalancingConfig: [], methodConfig: [ {