|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import { |
|
LoadBalancer, |
|
ChannelControlHelper, |
|
LoadBalancingConfig, |
|
registerDefaultLoadBalancerType, |
|
registerLoadBalancerType, |
|
} from './load-balancer'; |
|
import { ConnectivityState } from './connectivity-state'; |
|
import { |
|
QueuePicker, |
|
Picker, |
|
PickArgs, |
|
CompletePickResult, |
|
PickResultType, |
|
UnavailablePicker, |
|
} from './picker'; |
|
import { SubchannelAddress } from './subchannel-address'; |
|
import * as logging from './logging'; |
|
import { LogVerbosity } from './constants'; |
|
import { |
|
SubchannelInterface, |
|
ConnectivityStateListener, |
|
} from './subchannel-interface'; |
|
|
|
const TRACER_NAME = 'pick_first'; |
|
|
|
function trace(text: string): void { |
|
logging.trace(LogVerbosity.DEBUG, TRACER_NAME, text); |
|
} |
|
|
|
const TYPE_NAME = 'pick_first'; |
|
|
|
|
|
|
|
|
|
|
|
const CONNECTION_DELAY_INTERVAL_MS = 250; |
|
|
|
export class PickFirstLoadBalancingConfig implements LoadBalancingConfig { |
|
constructor(private readonly shuffleAddressList: boolean) {} |
|
|
|
getLoadBalancerName(): string { |
|
return TYPE_NAME; |
|
} |
|
|
|
toJsonObject(): object { |
|
return { |
|
[TYPE_NAME]: { |
|
shuffleAddressList: this.shuffleAddressList, |
|
}, |
|
}; |
|
} |
|
|
|
getShuffleAddressList() { |
|
return this.shuffleAddressList; |
|
} |
|
|
|
|
|
static createFromJson(obj: any) { |
|
if ( |
|
'shuffleAddressList' in obj && |
|
!(typeof obj.shuffleAddressList === 'boolean') |
|
) { |
|
throw new Error( |
|
'pick_first config field shuffleAddressList must be a boolean if provided' |
|
); |
|
} |
|
return new PickFirstLoadBalancingConfig(obj.shuffleAddressList === true); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
class PickFirstPicker implements Picker { |
|
constructor(private subchannel: SubchannelInterface) {} |
|
|
|
pick(pickArgs: PickArgs): CompletePickResult { |
|
return { |
|
pickResultType: PickResultType.COMPLETE, |
|
subchannel: this.subchannel, |
|
status: null, |
|
onCallStarted: null, |
|
onCallEnded: null, |
|
}; |
|
} |
|
} |
|
|
|
interface SubchannelChild { |
|
subchannel: SubchannelInterface; |
|
hasReportedTransientFailure: boolean; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
export function shuffled<T>(list: T[]): T[] { |
|
const result = list.slice(); |
|
for (let i = result.length - 1; i > 1; i--) { |
|
const j = Math.floor(Math.random() * (i + 1)); |
|
const temp = result[i]; |
|
result[i] = result[j]; |
|
result[j] = temp; |
|
} |
|
return result; |
|
} |
|
|
|
export class PickFirstLoadBalancer implements LoadBalancer { |
|
|
|
|
|
|
|
|
|
private children: SubchannelChild[] = []; |
|
|
|
|
|
|
|
private currentState: ConnectivityState = ConnectivityState.IDLE; |
|
|
|
|
|
|
|
|
|
private currentSubchannelIndex = 0; |
|
|
|
|
|
|
|
|
|
|
|
private currentPick: SubchannelInterface | null = null; |
|
|
|
|
|
|
|
|
|
private subchannelStateListener: ConnectivityStateListener = ( |
|
subchannel, |
|
previousState, |
|
newState, |
|
keepaliveTime, |
|
errorMessage |
|
) => { |
|
this.onSubchannelStateUpdate(subchannel, previousState, newState, errorMessage); |
|
}; |
|
|
|
|
|
|
|
private connectionDelayTimeout: NodeJS.Timeout; |
|
|
|
private triedAllSubchannels = false; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private stickyTransientFailureMode = false; |
|
|
|
|
|
|
|
|
|
|
|
private requestedResolutionSinceLastUpdate = false; |
|
|
|
|
|
|
|
|
|
|
|
private lastError: string | null = null; |
|
|
|
private latestAddressList: SubchannelAddress[] | null = null; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
constructor(private readonly channelControlHelper: ChannelControlHelper) { |
|
this.connectionDelayTimeout = setTimeout(() => {}, 0); |
|
clearTimeout(this.connectionDelayTimeout); |
|
} |
|
|
|
private allChildrenHaveReportedTF(): boolean { |
|
return this.children.every(child => child.hasReportedTransientFailure); |
|
} |
|
|
|
private calculateAndReportNewState() { |
|
if (this.currentPick) { |
|
this.updateState( |
|
ConnectivityState.READY, |
|
new PickFirstPicker(this.currentPick) |
|
); |
|
} else if (this.children.length === 0) { |
|
this.updateState(ConnectivityState.IDLE, new QueuePicker(this)); |
|
} else { |
|
if (this.stickyTransientFailureMode) { |
|
this.updateState( |
|
ConnectivityState.TRANSIENT_FAILURE, |
|
new UnavailablePicker({details: `No connection established. Last error: ${this.lastError}`}) |
|
); |
|
} else { |
|
this.updateState(ConnectivityState.CONNECTING, new QueuePicker(this)); |
|
} |
|
} |
|
} |
|
|
|
private requestReresolution() { |
|
this.requestedResolutionSinceLastUpdate = true; |
|
this.channelControlHelper.requestReresolution(); |
|
} |
|
|
|
private maybeEnterStickyTransientFailureMode() { |
|
if (!this.allChildrenHaveReportedTF()) { |
|
return; |
|
} |
|
if (!this.requestedResolutionSinceLastUpdate) { |
|
|
|
|
|
|
|
|
|
|
|
|
|
this.requestReresolution(); |
|
} |
|
if (this.stickyTransientFailureMode) { |
|
return; |
|
} |
|
this.stickyTransientFailureMode = true; |
|
for (const { subchannel } of this.children) { |
|
subchannel.startConnecting(); |
|
} |
|
this.calculateAndReportNewState(); |
|
} |
|
|
|
private removeCurrentPick() { |
|
if (this.currentPick !== null) { |
|
|
|
|
|
|
|
const currentPick = this.currentPick; |
|
this.currentPick = null; |
|
currentPick.unref(); |
|
currentPick.removeConnectivityStateListener(this.subchannelStateListener); |
|
this.channelControlHelper.removeChannelzChild( |
|
currentPick.getChannelzRef() |
|
); |
|
} |
|
} |
|
|
|
private onSubchannelStateUpdate( |
|
subchannel: SubchannelInterface, |
|
previousState: ConnectivityState, |
|
newState: ConnectivityState, |
|
errorMessage?: string |
|
) { |
|
if (this.currentPick?.realSubchannelEquals(subchannel)) { |
|
if (newState !== ConnectivityState.READY) { |
|
this.removeCurrentPick(); |
|
this.calculateAndReportNewState(); |
|
this.requestReresolution(); |
|
} |
|
return; |
|
} |
|
for (const [index, child] of this.children.entries()) { |
|
if (subchannel.realSubchannelEquals(child.subchannel)) { |
|
if (newState === ConnectivityState.READY) { |
|
this.pickSubchannel(child.subchannel); |
|
} |
|
if (newState === ConnectivityState.TRANSIENT_FAILURE) { |
|
child.hasReportedTransientFailure = true; |
|
if (errorMessage) { |
|
this.lastError = errorMessage; |
|
} |
|
this.maybeEnterStickyTransientFailureMode(); |
|
if (index === this.currentSubchannelIndex) { |
|
this.startNextSubchannelConnecting(index + 1); |
|
} |
|
} |
|
child.subchannel.startConnecting(); |
|
return; |
|
} |
|
} |
|
} |
|
|
|
private startNextSubchannelConnecting(startIndex: number) { |
|
clearTimeout(this.connectionDelayTimeout); |
|
if (this.triedAllSubchannels) { |
|
return; |
|
} |
|
for (const [index, child] of this.children.entries()) { |
|
if (index >= startIndex) { |
|
const subchannelState = child.subchannel.getConnectivityState(); |
|
if ( |
|
subchannelState === ConnectivityState.IDLE || |
|
subchannelState === ConnectivityState.CONNECTING |
|
) { |
|
this.startConnecting(index); |
|
return; |
|
} |
|
} |
|
} |
|
this.triedAllSubchannels = true; |
|
this.maybeEnterStickyTransientFailureMode(); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
private startConnecting(subchannelIndex: number) { |
|
clearTimeout(this.connectionDelayTimeout); |
|
this.currentSubchannelIndex = subchannelIndex; |
|
if ( |
|
this.children[subchannelIndex].subchannel.getConnectivityState() === |
|
ConnectivityState.IDLE |
|
) { |
|
trace( |
|
'Start connecting to subchannel with address ' + |
|
this.children[subchannelIndex].subchannel.getAddress() |
|
); |
|
process.nextTick(() => { |
|
this.children[subchannelIndex]?.subchannel.startConnecting(); |
|
}); |
|
} |
|
this.connectionDelayTimeout = setTimeout(() => { |
|
this.startNextSubchannelConnecting(subchannelIndex + 1); |
|
}, CONNECTION_DELAY_INTERVAL_MS).unref?.(); |
|
} |
|
|
|
private pickSubchannel(subchannel: SubchannelInterface) { |
|
if (this.currentPick && subchannel.realSubchannelEquals(this.currentPick)) { |
|
return; |
|
} |
|
trace('Pick subchannel with address ' + subchannel.getAddress()); |
|
this.stickyTransientFailureMode = false; |
|
if (this.currentPick !== null) { |
|
this.currentPick.unref(); |
|
this.channelControlHelper.removeChannelzChild( |
|
this.currentPick.getChannelzRef() |
|
); |
|
this.currentPick.removeConnectivityStateListener( |
|
this.subchannelStateListener |
|
); |
|
} |
|
this.currentPick = subchannel; |
|
subchannel.ref(); |
|
this.channelControlHelper.addChannelzChild(subchannel.getChannelzRef()); |
|
this.resetSubchannelList(); |
|
clearTimeout(this.connectionDelayTimeout); |
|
this.calculateAndReportNewState(); |
|
} |
|
|
|
private updateState(newState: ConnectivityState, picker: Picker) { |
|
trace( |
|
ConnectivityState[this.currentState] + |
|
' -> ' + |
|
ConnectivityState[newState] |
|
); |
|
this.currentState = newState; |
|
this.channelControlHelper.updateState(newState, picker); |
|
} |
|
|
|
private resetSubchannelList() { |
|
for (const child of this.children) { |
|
if (!(this.currentPick && child.subchannel.realSubchannelEquals(this.currentPick))) { |
|
|
|
|
|
|
|
|
|
child.subchannel.removeConnectivityStateListener( |
|
this.subchannelStateListener |
|
); |
|
} |
|
|
|
|
|
|
|
|
|
child.subchannel.unref(); |
|
this.channelControlHelper.removeChannelzChild( |
|
child.subchannel.getChannelzRef() |
|
); |
|
} |
|
this.currentSubchannelIndex = 0; |
|
this.children = []; |
|
this.triedAllSubchannels = false; |
|
this.requestedResolutionSinceLastUpdate = false; |
|
} |
|
|
|
private connectToAddressList(addressList: SubchannelAddress[]) { |
|
const newChildrenList = addressList.map(address => ({ |
|
subchannel: this.channelControlHelper.createSubchannel(address, {}), |
|
hasReportedTransientFailure: false, |
|
})); |
|
|
|
|
|
|
|
for (const { subchannel } of newChildrenList) { |
|
subchannel.ref(); |
|
this.channelControlHelper.addChannelzChild(subchannel.getChannelzRef()); |
|
} |
|
this.resetSubchannelList(); |
|
this.children = newChildrenList; |
|
for (const { subchannel } of this.children) { |
|
subchannel.addConnectivityStateListener(this.subchannelStateListener); |
|
if (subchannel.getConnectivityState() === ConnectivityState.READY) { |
|
this.pickSubchannel(subchannel); |
|
return; |
|
} |
|
} |
|
for (const child of this.children) { |
|
if ( |
|
child.subchannel.getConnectivityState() === |
|
ConnectivityState.TRANSIENT_FAILURE |
|
) { |
|
child.hasReportedTransientFailure = true; |
|
} |
|
} |
|
this.startNextSubchannelConnecting(0); |
|
this.calculateAndReportNewState(); |
|
} |
|
|
|
updateAddressList( |
|
addressList: SubchannelAddress[], |
|
lbConfig: LoadBalancingConfig |
|
): void { |
|
if (!(lbConfig instanceof PickFirstLoadBalancingConfig)) { |
|
return; |
|
} |
|
|
|
|
|
|
|
if (lbConfig.getShuffleAddressList()) { |
|
addressList = shuffled(addressList); |
|
} |
|
this.latestAddressList = addressList; |
|
this.connectToAddressList(addressList); |
|
} |
|
|
|
exitIdle() { |
|
if (this.currentState === ConnectivityState.IDLE && this.latestAddressList) { |
|
this.connectToAddressList(this.latestAddressList); |
|
} |
|
} |
|
|
|
resetBackoff() { |
|
|
|
|
|
} |
|
|
|
destroy() { |
|
this.resetSubchannelList(); |
|
this.removeCurrentPick(); |
|
} |
|
|
|
getTypeName(): string { |
|
return TYPE_NAME; |
|
} |
|
} |
|
|
|
export function setup(): void { |
|
registerLoadBalancerType( |
|
TYPE_NAME, |
|
PickFirstLoadBalancer, |
|
PickFirstLoadBalancingConfig |
|
); |
|
registerDefaultLoadBalancerType(TYPE_NAME); |
|
} |
|
|