| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import { |
| ChannelControlHelper, |
| LoadBalancer, |
| TypedLoadBalancingConfig, |
| selectLbConfigFromList, |
| } 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 { Endpoint } from './subchannel-address'; |
| import { GrpcUri, uriToString } from './uri-parser'; |
| import { ChildLoadBalancerHandler } from './load-balancer-child-handler'; |
| import { ChannelOptions } from './channel-options'; |
| import { ChannelCredentials } from './channel-credentials'; |
|
|
| 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'; |
|
|
| |
| |
| |
| |
| 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) { |
| |
| |
| |
| |
| |
| |
| 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 { |
| |
| |
| |
| private readonly innerResolver: Resolver; |
|
|
| private readonly childLoadBalancer: ChildLoadBalancerHandler; |
| private latestChildState: ConnectivityState = ConnectivityState.IDLE; |
| private latestChildPicker: Picker = new QueuePicker(this); |
| |
| |
| |
| private currentState: ConnectivityState = ConnectivityState.IDLE; |
| private readonly defaultServiceConfig: ServiceConfig; |
| |
| |
| |
| |
| |
| private previousServiceConfig: ServiceConfig | null = null; |
|
|
| |
| |
| |
| private readonly backoffTimeout: BackoffTimeout; |
|
|
| |
| |
| |
| |
| private continueResolving = false; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| constructor( |
| private readonly target: GrpcUri, |
| private readonly channelControlHelper: ChannelControlHelper, |
| credentials: ChannelCredentials, |
| 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 (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), |
| }, |
| credentials, |
| channelOptions |
| ); |
| this.innerResolver = createResolver( |
| target, |
| { |
| onSuccessfulResolution: ( |
| endpointList: Endpoint[], |
| serviceConfig: ServiceConfig | null, |
| serviceConfigError: ServiceError | null, |
| configSelector: ConfigSelector | null, |
| attributes: { [key: string]: unknown } |
| ) => { |
| this.backoffTimeout.stop(); |
| this.backoffTimeout.reset(); |
| let workingServiceConfig: ServiceConfig | null = null; |
| |
| |
| |
| |
| if (serviceConfig === null) { |
| |
| if (serviceConfigError === null) { |
| |
| this.previousServiceConfig = null; |
| workingServiceConfig = this.defaultServiceConfig; |
| } else { |
| |
| if (this.previousServiceConfig === null) { |
| |
| this.handleResolutionFailure(serviceConfigError); |
| } else { |
| |
| workingServiceConfig = this.previousServiceConfig; |
| } |
| } |
| } else { |
| |
| workingServiceConfig = serviceConfig; |
| this.previousServiceConfig = serviceConfig; |
| } |
| const workingConfigList = |
| workingServiceConfig?.loadBalancingConfig ?? []; |
| const loadBalancingConfig = selectLbConfigFromList( |
| workingConfigList, |
| true |
| ); |
| if (loadBalancingConfig === null) { |
| |
| this.handleResolutionFailure({ |
| code: Status.UNAVAILABLE, |
| details: |
| 'All load balancer options in service config are not compatible', |
| metadata: new Metadata(), |
| }); |
| return; |
| } |
| this.childLoadBalancer.updateAddressList( |
| endpointList, |
| 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, this.latestChildPicker); |
| } |
| this.backoffTimeout.runOnce(); |
| } |
|
|
| private updateState(connectivityState: ConnectivityState, picker: Picker) { |
| trace( |
| uriToString(this.target) + |
| ' ' + |
| ConnectivityState[this.currentState] + |
| ' -> ' + |
| ConnectivityState[connectivityState] |
| ); |
| |
| if (connectivityState === ConnectivityState.IDLE) { |
| picker = new QueuePicker(this, picker); |
| } |
| 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( |
| endpointList: Endpoint[], |
| lbConfig: TypedLoadBalancingConfig | 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'; |
| } |
| } |
|
|