RVC_RULE1 / tests /node_modules /@grpc /grpc-js /src /resolving-load-balancer.ts
sjufan84's picture
updated UI
77731d1
raw
history blame
13.3 kB
/*
* Copyright 2019 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
import {
ChannelControlHelper,
LoadBalancer,
LoadBalancingConfig,
getFirstUsableConfig,
} from './load-balancer';
import {
MethodConfig,
ServiceConfig,
validateServiceConfig,
} from './service-config';
import { ConnectivityState } from './connectivity-state';
import { ConfigSelector, createResolver, Resolver } from './resolver';
import { ServiceError } from './call';
import { Picker, UnavailablePicker, QueuePicker } from './picker';
import { BackoffOptions, BackoffTimeout } from './backoff-timeout';
import { Status } from './constants';
import { StatusObject } from './call-interface';
import { Metadata } from './metadata';
import * as logging from './logging';
import { LogVerbosity } from './constants';
import { SubchannelAddress } from './subchannel-address';
import { GrpcUri, uriToString } from './uri-parser';
import { ChildLoadBalancerHandler } from './load-balancer-child-handler';
import { ChannelOptions } from './channel-options';
const TRACER_NAME = 'resolving_load_balancer';
function trace(text: string): void {
logging.trace(LogVerbosity.DEBUG, TRACER_NAME, text);
}
type NameMatchLevel = 'EMPTY' | 'SERVICE' | 'SERVICE_AND_METHOD';
/**
* Name match levels in order from most to least specific. This is the order in
* which searches will be performed.
*/
const NAME_MATCH_LEVEL_ORDER: NameMatchLevel[] = [
'SERVICE_AND_METHOD',
'SERVICE',
'EMPTY',
];
function hasMatchingName(
service: string,
method: string,
methodConfig: MethodConfig,
matchLevel: NameMatchLevel
): boolean {
for (const name of methodConfig.name) {
switch (matchLevel) {
case 'EMPTY':
if (!name.service && !name.method) {
return true;
}
break;
case 'SERVICE':
if (name.service === service && !name.method) {
return true;
}
break;
case 'SERVICE_AND_METHOD':
if (name.service === service && name.method === method) {
return true;
}
}
}
return false;
}
function findMatchingConfig(
service: string,
method: string,
methodConfigs: MethodConfig[],
matchLevel: NameMatchLevel
): MethodConfig | null {
for (const config of methodConfigs) {
if (hasMatchingName(service, method, config, matchLevel)) {
return config;
}
}
return null;
}
function getDefaultConfigSelector(
serviceConfig: ServiceConfig | null
): ConfigSelector {
return function defaultConfigSelector(
methodName: string,
metadata: Metadata
) {
const splitName = methodName.split('/').filter(x => x.length > 0);
const service = splitName[0] ?? '';
const method = splitName[1] ?? '';
if (serviceConfig && serviceConfig.methodConfig) {
/* Check for the following in order, and return the first method
* config that matches:
* 1. A name that exactly matches the service and method
* 2. A name with no method set that matches the service
* 3. An empty name
*/
for (const matchLevel of NAME_MATCH_LEVEL_ORDER) {
const matchingConfig = findMatchingConfig(
service,
method,
serviceConfig.methodConfig,
matchLevel
);
if (matchingConfig) {
return {
methodConfig: matchingConfig,
pickInformation: {},
status: Status.OK,
dynamicFilterFactories: [],
};
}
}
}
return {
methodConfig: { name: [] },
pickInformation: {},
status: Status.OK,
dynamicFilterFactories: [],
};
};
}
export interface ResolutionCallback {
(serviceConfig: ServiceConfig, configSelector: ConfigSelector): void;
}
export interface ResolutionFailureCallback {
(status: StatusObject): void;
}
export class ResolvingLoadBalancer implements LoadBalancer {
/**
* The resolver class constructed for the target address.
*/
private readonly innerResolver: Resolver;
private readonly childLoadBalancer: ChildLoadBalancerHandler;
private latestChildState: ConnectivityState = ConnectivityState.IDLE;
private latestChildPicker: Picker = new QueuePicker(this);
/**
* This resolving load balancer's current connectivity state.
*/
private currentState: ConnectivityState = ConnectivityState.IDLE;
private readonly defaultServiceConfig: ServiceConfig;
/**
* The service config object from the last successful resolution, if
* available. A value of null indicates that we have not yet received a valid
* service config from the resolver.
*/
private previousServiceConfig: ServiceConfig | null = null;
/**
* The backoff timer for handling name resolution failures.
*/
private readonly backoffTimeout: BackoffTimeout;
/**
* Indicates whether we should attempt to resolve again after the backoff
* timer runs out.
*/
private continueResolving = false;
/**
* Wrapper class that behaves like a `LoadBalancer` and also handles name
* resolution internally.
* @param target The address of the backend to connect to.
* @param channelControlHelper `ChannelControlHelper` instance provided by
* this load balancer's owner.
* @param defaultServiceConfig The default service configuration to be used
* if none is provided by the name resolver. A `null` value indicates
* that the default behavior should be the default unconfigured behavior.
* In practice, that means using the "pick first" load balancer
* implmentation
*/
constructor(
private readonly target: GrpcUri,
private readonly channelControlHelper: ChannelControlHelper,
channelOptions: ChannelOptions,
private readonly onSuccessfulResolution: ResolutionCallback,
private readonly onFailedResolution: ResolutionFailureCallback
) {
if (channelOptions['grpc.service_config']) {
this.defaultServiceConfig = validateServiceConfig(
JSON.parse(channelOptions['grpc.service_config']!)
);
} else {
this.defaultServiceConfig = {
loadBalancingConfig: [],
methodConfig: [],
};
}
this.updateState(ConnectivityState.IDLE, new QueuePicker(this));
this.childLoadBalancer = new ChildLoadBalancerHandler({
createSubchannel:
channelControlHelper.createSubchannel.bind(channelControlHelper),
requestReresolution: () => {
/* If the backoffTimeout is running, we're still backing off from
* making resolve requests, so we shouldn't make another one here.
* In that case, the backoff timer callback will call
* updateResolution */
if (this.backoffTimeout.isRunning()) {
trace('requestReresolution delayed by backoff timer until ' + this.backoffTimeout.getEndTime().toISOString());
this.continueResolving = true;
} else {
this.updateResolution();
}
},
updateState: (newState: ConnectivityState, picker: Picker) => {
this.latestChildState = newState;
this.latestChildPicker = picker;
this.updateState(newState, picker);
},
addChannelzChild:
channelControlHelper.addChannelzChild.bind(channelControlHelper),
removeChannelzChild:
channelControlHelper.removeChannelzChild.bind(channelControlHelper),
});
this.innerResolver = createResolver(
target,
{
onSuccessfulResolution: (
addressList: SubchannelAddress[],
serviceConfig: ServiceConfig | null,
serviceConfigError: ServiceError | null,
configSelector: ConfigSelector | null,
attributes: { [key: string]: unknown }
) => {
this.backoffTimeout.stop();
this.backoffTimeout.reset();
let workingServiceConfig: ServiceConfig | null = null;
/* This first group of conditionals implements the algorithm described
* in https://github.com/grpc/proposal/blob/master/A21-service-config-error-handling.md
* in the section called "Behavior on receiving a new gRPC Config".
*/
if (serviceConfig === null) {
// Step 4 and 5
if (serviceConfigError === null) {
// Step 5
this.previousServiceConfig = null;
workingServiceConfig = this.defaultServiceConfig;
} else {
// Step 4
if (this.previousServiceConfig === null) {
// Step 4.ii
this.handleResolutionFailure(serviceConfigError);
} else {
// Step 4.i
workingServiceConfig = this.previousServiceConfig;
}
}
} else {
// Step 3
workingServiceConfig = serviceConfig;
this.previousServiceConfig = serviceConfig;
}
const workingConfigList =
workingServiceConfig?.loadBalancingConfig ?? [];
const loadBalancingConfig = getFirstUsableConfig(
workingConfigList,
true
);
if (loadBalancingConfig === null) {
// There were load balancing configs but none are supported. This counts as a resolution failure
this.handleResolutionFailure({
code: Status.UNAVAILABLE,
details:
'All load balancer options in service config are not compatible',
metadata: new Metadata(),
});
return;
}
this.childLoadBalancer.updateAddressList(
addressList,
loadBalancingConfig,
attributes
);
const finalServiceConfig =
workingServiceConfig ?? this.defaultServiceConfig;
this.onSuccessfulResolution(
finalServiceConfig,
configSelector ?? getDefaultConfigSelector(finalServiceConfig)
);
},
onError: (error: StatusObject) => {
this.handleResolutionFailure(error);
},
},
channelOptions
);
const backoffOptions: BackoffOptions = {
initialDelay: channelOptions['grpc.initial_reconnect_backoff_ms'],
maxDelay: channelOptions['grpc.max_reconnect_backoff_ms'],
};
this.backoffTimeout = new BackoffTimeout(() => {
if (this.continueResolving) {
this.updateResolution();
this.continueResolving = false;
} else {
this.updateState(this.latestChildState, this.latestChildPicker);
}
}, backoffOptions);
this.backoffTimeout.unref();
}
private updateResolution() {
this.innerResolver.updateResolution();
if (this.currentState === ConnectivityState.IDLE) {
this.updateState(ConnectivityState.CONNECTING, new QueuePicker(this));
}
this.backoffTimeout.runOnce();
}
private updateState(connectivityState: ConnectivityState, picker: Picker) {
trace(
uriToString(this.target) +
' ' +
ConnectivityState[this.currentState] +
' -> ' +
ConnectivityState[connectivityState]
);
// Ensure that this.exitIdle() is called by the picker
if (connectivityState === ConnectivityState.IDLE) {
picker = new QueuePicker(this);
}
this.currentState = connectivityState;
this.channelControlHelper.updateState(connectivityState, picker);
}
private handleResolutionFailure(error: StatusObject) {
if (this.latestChildState === ConnectivityState.IDLE) {
this.updateState(
ConnectivityState.TRANSIENT_FAILURE,
new UnavailablePicker(error)
);
this.onFailedResolution(error);
}
}
exitIdle() {
if (
this.currentState === ConnectivityState.IDLE ||
this.currentState === ConnectivityState.TRANSIENT_FAILURE
) {
if (this.backoffTimeout.isRunning()) {
this.continueResolving = true;
} else {
this.updateResolution();
}
}
this.childLoadBalancer.exitIdle();
}
updateAddressList(
addressList: SubchannelAddress[],
lbConfig: LoadBalancingConfig | null
): never {
throw new Error('updateAddressList not supported on ResolvingLoadBalancer');
}
resetBackoff() {
this.backoffTimeout.reset();
this.childLoadBalancer.resetBackoff();
}
destroy() {
this.childLoadBalancer.destroy();
this.innerResolver.destroy();
this.backoffTimeout.reset();
this.backoffTimeout.stop();
this.latestChildState = ConnectivityState.IDLE;
this.latestChildPicker = new QueuePicker(this);
this.currentState = ConnectivityState.IDLE;
this.previousServiceConfig = null;
this.continueResolving = false;
}
getTypeName() {
return 'resolving_load_balancer';
}
}