|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import { |
|
LoadBalancer, |
|
ChannelControlHelper, |
|
LoadBalancingConfig, |
|
registerLoadBalancerType, |
|
} from './load-balancer'; |
|
import { ConnectivityState } from './connectivity-state'; |
|
import { |
|
QueuePicker, |
|
Picker, |
|
PickArgs, |
|
CompletePickResult, |
|
PickResultType, |
|
UnavailablePicker, |
|
} from './picker'; |
|
import { |
|
SubchannelAddress, |
|
subchannelAddressToString, |
|
} from './subchannel-address'; |
|
import * as logging from './logging'; |
|
import { LogVerbosity } from './constants'; |
|
import { |
|
ConnectivityStateListener, |
|
SubchannelInterface, |
|
} from './subchannel-interface'; |
|
|
|
const TRACER_NAME = 'round_robin'; |
|
|
|
function trace(text: string): void { |
|
logging.trace(LogVerbosity.DEBUG, TRACER_NAME, text); |
|
} |
|
|
|
const TYPE_NAME = 'round_robin'; |
|
|
|
class RoundRobinLoadBalancingConfig implements LoadBalancingConfig { |
|
getLoadBalancerName(): string { |
|
return TYPE_NAME; |
|
} |
|
|
|
constructor() {} |
|
|
|
toJsonObject(): object { |
|
return { |
|
[TYPE_NAME]: {}, |
|
}; |
|
} |
|
|
|
|
|
static createFromJson(obj: any) { |
|
return new RoundRobinLoadBalancingConfig(); |
|
} |
|
} |
|
|
|
class RoundRobinPicker implements Picker { |
|
constructor( |
|
private readonly subchannelList: SubchannelInterface[], |
|
private nextIndex = 0 |
|
) {} |
|
|
|
pick(pickArgs: PickArgs): CompletePickResult { |
|
const pickedSubchannel = this.subchannelList[this.nextIndex]; |
|
this.nextIndex = (this.nextIndex + 1) % this.subchannelList.length; |
|
return { |
|
pickResultType: PickResultType.COMPLETE, |
|
subchannel: pickedSubchannel, |
|
status: null, |
|
onCallStarted: null, |
|
onCallEnded: null, |
|
}; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
peekNextSubchannel(): SubchannelInterface { |
|
return this.subchannelList[this.nextIndex]; |
|
} |
|
} |
|
|
|
export class RoundRobinLoadBalancer implements LoadBalancer { |
|
private subchannels: SubchannelInterface[] = []; |
|
|
|
private currentState: ConnectivityState = ConnectivityState.IDLE; |
|
|
|
private subchannelStateListener: ConnectivityStateListener; |
|
|
|
private currentReadyPicker: RoundRobinPicker | null = null; |
|
|
|
private lastError: string | null = null; |
|
|
|
constructor(private readonly channelControlHelper: ChannelControlHelper) { |
|
this.subchannelStateListener = ( |
|
subchannel: SubchannelInterface, |
|
previousState: ConnectivityState, |
|
newState: ConnectivityState, |
|
keepaliveTime: number, |
|
errorMessage?: string |
|
) => { |
|
this.calculateAndUpdateState(); |
|
if ( |
|
newState === ConnectivityState.TRANSIENT_FAILURE || |
|
newState === ConnectivityState.IDLE |
|
) { |
|
if (errorMessage) { |
|
this.lastError = errorMessage; |
|
} |
|
this.channelControlHelper.requestReresolution(); |
|
subchannel.startConnecting(); |
|
} |
|
}; |
|
} |
|
|
|
private countSubchannelsWithState(state: ConnectivityState) { |
|
return this.subchannels.filter( |
|
subchannel => subchannel.getConnectivityState() === state |
|
).length; |
|
} |
|
|
|
private calculateAndUpdateState() { |
|
if (this.countSubchannelsWithState(ConnectivityState.READY) > 0) { |
|
const readySubchannels = this.subchannels.filter( |
|
subchannel => |
|
subchannel.getConnectivityState() === ConnectivityState.READY |
|
); |
|
let index = 0; |
|
if (this.currentReadyPicker !== null) { |
|
index = readySubchannels.indexOf( |
|
this.currentReadyPicker.peekNextSubchannel() |
|
); |
|
if (index < 0) { |
|
index = 0; |
|
} |
|
} |
|
this.updateState( |
|
ConnectivityState.READY, |
|
new RoundRobinPicker(readySubchannels, index) |
|
); |
|
} else if ( |
|
this.countSubchannelsWithState(ConnectivityState.CONNECTING) > 0 |
|
) { |
|
this.updateState(ConnectivityState.CONNECTING, new QueuePicker(this)); |
|
} else if ( |
|
this.countSubchannelsWithState(ConnectivityState.TRANSIENT_FAILURE) > 0 |
|
) { |
|
this.updateState( |
|
ConnectivityState.TRANSIENT_FAILURE, |
|
new UnavailablePicker({details: `No connection established. Last error: ${this.lastError}`}) |
|
); |
|
} else { |
|
this.updateState(ConnectivityState.IDLE, new QueuePicker(this)); |
|
} |
|
} |
|
|
|
private updateState(newState: ConnectivityState, picker: Picker) { |
|
trace( |
|
ConnectivityState[this.currentState] + |
|
' -> ' + |
|
ConnectivityState[newState] |
|
); |
|
if (newState === ConnectivityState.READY) { |
|
this.currentReadyPicker = picker as RoundRobinPicker; |
|
} else { |
|
this.currentReadyPicker = null; |
|
} |
|
this.currentState = newState; |
|
this.channelControlHelper.updateState(newState, picker); |
|
} |
|
|
|
private resetSubchannelList() { |
|
for (const subchannel of this.subchannels) { |
|
subchannel.removeConnectivityStateListener(this.subchannelStateListener); |
|
subchannel.unref(); |
|
this.channelControlHelper.removeChannelzChild( |
|
subchannel.getChannelzRef() |
|
); |
|
} |
|
this.subchannels = []; |
|
} |
|
|
|
updateAddressList( |
|
addressList: SubchannelAddress[], |
|
lbConfig: LoadBalancingConfig |
|
): void { |
|
this.resetSubchannelList(); |
|
trace( |
|
'Connect to address list ' + |
|
addressList.map(address => subchannelAddressToString(address)) |
|
); |
|
this.subchannels = addressList.map(address => |
|
this.channelControlHelper.createSubchannel(address, {}) |
|
); |
|
for (const subchannel of this.subchannels) { |
|
subchannel.ref(); |
|
subchannel.addConnectivityStateListener(this.subchannelStateListener); |
|
this.channelControlHelper.addChannelzChild(subchannel.getChannelzRef()); |
|
const subchannelState = subchannel.getConnectivityState(); |
|
if ( |
|
subchannelState === ConnectivityState.IDLE || |
|
subchannelState === ConnectivityState.TRANSIENT_FAILURE |
|
) { |
|
subchannel.startConnecting(); |
|
} |
|
} |
|
this.calculateAndUpdateState(); |
|
} |
|
|
|
exitIdle(): void { |
|
for (const subchannel of this.subchannels) { |
|
subchannel.startConnecting(); |
|
} |
|
} |
|
resetBackoff(): void { |
|
|
|
|
|
} |
|
destroy(): void { |
|
this.resetSubchannelList(); |
|
} |
|
getTypeName(): string { |
|
return TYPE_NAME; |
|
} |
|
} |
|
|
|
export function setup() { |
|
registerLoadBalancerType( |
|
TYPE_NAME, |
|
RoundRobinLoadBalancer, |
|
RoundRobinLoadBalancingConfig |
|
); |
|
} |
|
|