/* * 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, isLoadBalancerNameRegistered, createLoadBalancer, } from './load-balancer'; import { ServiceConfig } from './service-config'; import { ConnectivityState } from './channel'; import { createResolver, Resolver } from './resolver'; import { ServiceError } from './call'; import { ChannelOptions } from './channel-options'; import { Picker, UnavailablePicker, QueuePicker } from './picker'; import { LoadBalancingConfig } from './load-balancing-config'; import { BackoffTimeout } from './backoff-timeout'; import { Status } from './constants'; import { StatusObject } from './call-stream'; import { Metadata } from './metadata'; import * as logging from './logging'; import { LogVerbosity } from './constants'; import { SubchannelAddress } from './subchannel'; import { GrpcUri, uriToString } from './uri-parser'; const TRACER_NAME = 'resolving_load_balancer'; function trace(text: string): void { logging.trace(LogVerbosity.DEBUG, TRACER_NAME, text); } const DEFAULT_LOAD_BALANCER_NAME = 'pick_first'; export class ResolvingLoadBalancer implements LoadBalancer { /** * The resolver class constructed for the target address. */ private innerResolver: Resolver; /** * Current internal load balancer used for handling calls. * Invariant: innerLoadBalancer === null => pendingReplacementLoadBalancer === null. */ private innerLoadBalancer: LoadBalancer | null = null; /** * The load balancer instance that will be used in place of the current * `innerLoadBalancer` once either that load balancer loses its connection * or this one establishes a connection. For use when a new name resolution * result comes in with a different load balancing configuration, and the * current `innerLoadBalancer` is still connected. */ private pendingReplacementLoadBalancer: LoadBalancer | null = null; /** * This resolving load balancer's current connectivity state. */ private currentState: ConnectivityState = ConnectivityState.IDLE; /** * The service config object from the last successful resolution, if * available. A value of undefined indicates that there has not yet * been a successful resolution. A value of null indicates that the last * successful resolution explicitly provided a null service config. */ private previousServiceConfig: ServiceConfig | null | undefined = undefined; /** * The most recently reported connectivity state of the `innerLoadBalancer`. */ private innerBalancerState: ConnectivityState = ConnectivityState.IDLE; private innerBalancerPicker: Picker = new UnavailablePicker(); /** * The most recent reported state of the pendingReplacementLoadBalancer. * Starts at IDLE for type simplicity. This should get updated as soon as the * pendingReplacementLoadBalancer gets constructed. */ private replacementBalancerState: ConnectivityState = ConnectivityState.IDLE; /** * The picker associated with the replacementBalancerState. Starts as an * UnavailablePicker for type simplicity. This should get updated as soon as * the pendingReplacementLoadBalancer gets constructed. */ private replacementBalancerPicker: Picker = new UnavailablePicker(); /** * ChannelControlHelper for the innerLoadBalancer. */ private readonly innerChannelControlHelper: ChannelControlHelper; /** * ChannelControlHelper for the pendingReplacementLoadBalancer. */ private readonly replacementChannelControlHelper: ChannelControlHelper; /** * 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 target: GrpcUri, private channelControlHelper: ChannelControlHelper, private defaultServiceConfig: ServiceConfig | null ) { this.updateState(ConnectivityState.IDLE, new QueuePicker(this)); this.innerResolver = createResolver(target, { onSuccessfulResolution: ( addressList: SubchannelAddress[], serviceConfig: ServiceConfig | null, serviceConfigError: ServiceError | null ) => { 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 = serviceConfig; workingServiceConfig = this.defaultServiceConfig; } else { // Step 4 if (this.previousServiceConfig === undefined) { // Step 4.ii if (this.defaultServiceConfig === null) { // Step 4.ii.b this.handleResolutionFailure(serviceConfigError); } else { // Step 4.ii.a workingServiceConfig = this.defaultServiceConfig; } } else { // Step 4.i workingServiceConfig = this.previousServiceConfig; } } } else { // Step 3 workingServiceConfig = serviceConfig; this.previousServiceConfig = serviceConfig; } let loadBalancerName: string | null = null; let loadBalancingConfig: LoadBalancingConfig | null = null; if ( workingServiceConfig === null || workingServiceConfig.loadBalancingConfig.length === 0 ) { loadBalancerName = DEFAULT_LOAD_BALANCER_NAME; } else { for (const lbConfig of workingServiceConfig.loadBalancingConfig) { // Iterating through a oneof looking for whichever one is populated for (const key in lbConfig) { if (Object.prototype.hasOwnProperty.call(lbConfig, key)) { if (isLoadBalancerNameRegistered(key)) { loadBalancerName = key; loadBalancingConfig = lbConfig; break; } } } if (loadBalancerName !== null) { break; } } if (loadBalancerName === 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; } } if (this.innerLoadBalancer === null) { this.innerLoadBalancer = createLoadBalancer( loadBalancerName, this.innerChannelControlHelper )!; this.innerLoadBalancer.updateAddressList( addressList, loadBalancingConfig ); } else if (this.innerLoadBalancer.getTypeName() === loadBalancerName) { this.innerLoadBalancer.updateAddressList( addressList, loadBalancingConfig ); } else { if ( this.pendingReplacementLoadBalancer === null || this.pendingReplacementLoadBalancer.getTypeName() !== loadBalancerName ) { if (this.pendingReplacementLoadBalancer !== null) { this.pendingReplacementLoadBalancer.destroy(); } this.pendingReplacementLoadBalancer = createLoadBalancer( loadBalancerName, this.replacementChannelControlHelper )!; } this.pendingReplacementLoadBalancer.updateAddressList( addressList, loadBalancingConfig ); } }, onError: (error: StatusObject) => { this.handleResolutionFailure(error); }, }); this.innerChannelControlHelper = { createSubchannel: ( subchannelAddress: SubchannelAddress, subchannelArgs: ChannelOptions ) => { return this.channelControlHelper.createSubchannel( subchannelAddress, subchannelArgs ); }, updateState: (connectivityState: ConnectivityState, picker: Picker) => { this.innerBalancerState = connectivityState; if (connectivityState === ConnectivityState.IDLE) { picker = new QueuePicker(this); } this.innerBalancerPicker = picker; if ( connectivityState !== ConnectivityState.READY && this.pendingReplacementLoadBalancer !== null ) { this.switchOverReplacementBalancer(); } else { if (connectivityState === ConnectivityState.IDLE) { if (this.innerLoadBalancer) { this.innerLoadBalancer.destroy(); this.innerLoadBalancer = null; } } this.updateState(connectivityState, picker); } }, requestReresolution: () => { if (this.pendingReplacementLoadBalancer === null) { /* 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()) { this.continueResolving = true; } else { this.updateResolution(); } } }, }; this.replacementChannelControlHelper = { createSubchannel: ( subchannelAddress: SubchannelAddress, subchannelArgs: ChannelOptions ) => { return this.channelControlHelper.createSubchannel( subchannelAddress, subchannelArgs ); }, updateState: (connectivityState: ConnectivityState, picker: Picker) => { if (connectivityState === ConnectivityState.IDLE) { picker = new QueuePicker(this); } this.replacementBalancerState = connectivityState; this.replacementBalancerPicker = picker; if (connectivityState === ConnectivityState.READY) { this.switchOverReplacementBalancer(); } else if (connectivityState === ConnectivityState.IDLE) { if (this.pendingReplacementLoadBalancer) { this.pendingReplacementLoadBalancer.destroy(); this.pendingReplacementLoadBalancer = null; } } }, 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()) { this.continueResolving = true; } else { this.updateResolution(); } }, }; this.backoffTimeout = new BackoffTimeout(() => { if (this.continueResolving) { this.updateResolution(); this.continueResolving = false; } else { if (this.innerLoadBalancer === null) { this.updateState(ConnectivityState.IDLE, new QueuePicker(this)); } else { this.updateState(this.innerBalancerState, this.innerBalancerPicker); } } }); } private updateResolution() { this.innerResolver.updateResolution(); if ( this.innerLoadBalancer === null || this.innerBalancerState === ConnectivityState.IDLE ) { this.updateState(ConnectivityState.CONNECTING, new QueuePicker(this)); } } private updateState(connectivitystate: ConnectivityState, picker: Picker) { trace( uriToString(this.target) + ' ' + ConnectivityState[this.currentState] + ' -> ' + ConnectivityState[connectivitystate] ); this.currentState = connectivitystate; this.channelControlHelper.updateState(connectivitystate, picker); } /** * Stop using the current innerLoadBalancer and replace it with the * pendingReplacementLoadBalancer. Must only be called if both of * those are currently not null. */ private switchOverReplacementBalancer() { this.innerLoadBalancer!.destroy(); this.innerLoadBalancer = this.pendingReplacementLoadBalancer!; this.innerLoadBalancer.replaceChannelControlHelper( this.innerChannelControlHelper ); this.pendingReplacementLoadBalancer = null; this.innerBalancerState = this.replacementBalancerState; this.innerBalancerPicker = this.replacementBalancerPicker; this.updateState( this.replacementBalancerState, this.replacementBalancerPicker ); } private handleResolutionFailure(error: StatusObject) { if ( this.innerLoadBalancer === null || this.innerBalancerState === ConnectivityState.IDLE ) { this.updateState( ConnectivityState.TRANSIENT_FAILURE, new UnavailablePicker(error) ); } this.backoffTimeout.runOnce(); } exitIdle() { if (this.innerLoadBalancer !== null) { this.innerLoadBalancer.exitIdle(); } if (this.currentState === ConnectivityState.IDLE) { if (this.backoffTimeout.isRunning()) { this.continueResolving = true; } else { this.updateResolution(); } this.updateState(ConnectivityState.CONNECTING, new QueuePicker(this)); } } updateAddressList( addressList: SubchannelAddress[], lbConfig: LoadBalancingConfig | null ) { throw new Error('updateAddressList not supported on ResolvingLoadBalancer'); } resetBackoff() { this.backoffTimeout.reset(); if (this.innerLoadBalancer !== null) { this.innerLoadBalancer.resetBackoff(); } if (this.pendingReplacementLoadBalancer !== null) { this.pendingReplacementLoadBalancer.resetBackoff(); } } destroy() { if (this.innerLoadBalancer !== null) { this.innerLoadBalancer.destroy(); this.innerLoadBalancer = null; } if (this.pendingReplacementLoadBalancer !== null) { this.pendingReplacementLoadBalancer.destroy(); this.pendingReplacementLoadBalancer = null; } this.updateState(ConnectivityState.SHUTDOWN, new UnavailablePicker()); } getTypeName() { return 'resolving_load_balancer'; } replaceChannelControlHelper(channelControlHelper: ChannelControlHelper) { this.channelControlHelper = channelControlHelper; } }