|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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'; |
|
|
|
|
|
|
|
|
|
|
|
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, |
|
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), |
|
}); |
|
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; |
|
|
|
|
|
|
|
|
|
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 = getFirstUsableConfig( |
|
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( |
|
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] |
|
); |
|
|
|
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'; |
|
} |
|
} |
|
|