grpc-node/packages/grpc-js-xds/src/load-balancer-lrs.ts

239 lines
8.5 KiB
TypeScript

/*
* 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, StatusObject, status as Status, experimental } from '@grpc/grpc-js';
import { Locality__Output } from './generated/envoy/api/v2/core/Locality';
import { XdsClusterLocalityStats, XdsClient, getSingletonXdsClient } from './xds-client';
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;
import PickResultType = experimental.PickResultType;
import PickResult = experimental.PickResult;
import Filter = experimental.Filter;
import BaseFilter = experimental.BaseFilter;
import FilterFactory = experimental.FilterFactory;
import FilterStackFactory = experimental.FilterStackFactory;
import Call = experimental.CallStream;
import validateLoadBalancingConfig = experimental.validateLoadBalancingConfig
const TYPE_NAME = 'lrs';
export class LrsLoadBalancingConfig implements LoadBalancingConfig {
getLoadBalancerName(): string {
return TYPE_NAME;
}
toJsonObject(): object {
return {
[TYPE_NAME]: {
cluster_name: this.clusterName,
eds_service_name: this.edsServiceName,
lrs_load_reporting_server_name: this.lrsLoadReportingServerName,
locality: this.locality,
child_policy: this.childPolicy.map(policy => policy.toJsonObject())
}
}
}
constructor(private clusterName: string, private edsServiceName: string, private lrsLoadReportingServerName: string, private locality: Locality__Output, private childPolicy: LoadBalancingConfig[]) {}
getClusterName() {
return this.clusterName;
}
getEdsServiceName() {
return this.edsServiceName;
}
getLrsLoadReportingServerName() {
return this.lrsLoadReportingServerName;
}
getLocality() {
return this.locality;
}
getChildPolicy() {
return this.childPolicy;
}
static createFromJson(obj: any): LrsLoadBalancingConfig {
if (!('cluster_name' in obj && typeof obj.cluster_name === 'string')) {
throw new Error('lrs config must have a string field cluster_name');
}
if (!('eds_service_name' in obj && typeof obj.eds_service_name === 'string')) {
throw new Error('lrs config must have a string field eds_service_name');
}
if (!('lrs_load_reporting_server_name' in obj && typeof obj.lrs_load_reporting_server_name === 'string')) {
throw new Error('lrs config must have a string field lrs_load_reporting_server_name');
}
if (!('locality' in obj && obj.locality !== null && typeof obj.locality === 'object')) {
throw new Error('lrs config must have an object field locality');
}
if ('region' in obj.locality && typeof obj.locality.region !== 'string') {
throw new Error('lrs config locality.region field must be a string if provided');
}
if ('zone' in obj.locality && typeof obj.locality.zone !== 'string') {
throw new Error('lrs config locality.zone field must be a string if provided');
}
if ('sub_zone' in obj.locality && typeof obj.locality.sub_zone !== 'string') {
throw new Error('lrs config locality.sub_zone field must be a string if provided');
}
if (!('child_policy' in obj && Array.isArray(obj.child_policy))) {
throw new Error('lrs config must have a child_policy array');
}
return new LrsLoadBalancingConfig(obj.cluster_name, obj.eds_service_name, obj.lrs_load_reporting_server_name, {
region: obj.locality.region ?? '',
zone: obj.locality.zone ?? '',
sub_zone: obj.locality.sub_zone ?? ''
}, obj.child_policy.map(validateLoadBalancingConfig));
}
}
/**
* Filter class that reports when the call ends.
*/
class CallEndTrackingFilter extends BaseFilter implements Filter {
constructor(private localityStatsReporter: XdsClusterLocalityStats) {
super();
}
receiveTrailers(status: StatusObject) {
this.localityStatsReporter.addCallFinished(status.code !== Status.OK);
return status;
}
}
class CallEndTrackingFilterFactory
implements FilterFactory<CallEndTrackingFilter> {
constructor(private localityStatsReporter: XdsClusterLocalityStats) {}
createFilter(callStream: Call): CallEndTrackingFilter {
return new CallEndTrackingFilter(this.localityStatsReporter);
}
}
/**
* Picker that delegates picking to another picker, and reports when calls
* created using those picks start and end.
*/
class LoadReportingPicker implements Picker {
constructor(
private wrappedPicker: Picker,
private localityStatsReporter: XdsClusterLocalityStats
) {}
pick(pickArgs: PickArgs): PickResult {
const wrappedPick = this.wrappedPicker.pick(pickArgs);
if (wrappedPick.pickResultType === PickResultType.COMPLETE) {
const trackingFilterFactory = new CallEndTrackingFilterFactory(
this.localityStatsReporter
);
/* In the unlikely event that the wrappedPick already has an
* extraFilterFactory, preserve it in a FilterStackFactory. */
const extraFilterFactory = wrappedPick.extraFilterFactory
? new FilterStackFactory([
wrappedPick.extraFilterFactory,
trackingFilterFactory,
])
: trackingFilterFactory;
return {
pickResultType: PickResultType.COMPLETE,
subchannel: wrappedPick.subchannel,
status: null,
onCallStarted: () => {
wrappedPick.onCallStarted?.();
this.localityStatsReporter.addCallStarted();
},
extraFilterFactory: extraFilterFactory,
};
} else {
return wrappedPick;
}
}
}
/**
* "Load balancer" that delegates the actual load balancing logic to another
* LoadBalancer class and adds hooks to track when calls started using that
* LoadBalancer start and end, and uses the XdsClient to report that
* information back to the xDS server.
*/
export class LrsLoadBalancer implements LoadBalancer {
private childBalancer: ChildLoadBalancerHandler;
private localityStatsReporter: XdsClusterLocalityStats | null = null;
constructor(private channelControlHelper: ChannelControlHelper) {
this.childBalancer = new ChildLoadBalancerHandler({
createSubchannel: (subchannelAddress, subchannelArgs) =>
channelControlHelper.createSubchannel(
subchannelAddress,
subchannelArgs
),
requestReresolution: () => channelControlHelper.requestReresolution(),
updateState: (connectivityState: ConnectivityState, picker: Picker) => {
if (this.localityStatsReporter !== null) {
picker = new LoadReportingPicker(picker, this.localityStatsReporter);
}
channelControlHelper.updateState(connectivityState, picker);
},
});
}
updateAddressList(
addressList: SubchannelAddress[],
lbConfig: LoadBalancingConfig,
attributes: { [key: string]: unknown }
): void {
if (!(lbConfig instanceof LrsLoadBalancingConfig)) {
return;
}
this.localityStatsReporter = getSingletonXdsClient().addClusterLocalityStats(
lbConfig.getLrsLoadReportingServerName(),
lbConfig.getClusterName(),
lbConfig.getEdsServiceName(),
lbConfig.getLocality()
);
const childPolicy: LoadBalancingConfig = getFirstUsableConfig(
lbConfig.getChildPolicy(),
true
);
this.childBalancer.updateAddressList(addressList, childPolicy, attributes);
}
exitIdle(): void {
this.childBalancer.exitIdle();
}
resetBackoff(): void {
this.childBalancer.resetBackoff();
}
destroy(): void {
this.childBalancer.destroy();
}
getTypeName(): string {
return TYPE_NAME;
}
}
export function setup() {
registerLoadBalancerType(TYPE_NAME, LrsLoadBalancer, LrsLoadBalancingConfig);
}