|
"use strict"; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Object.defineProperty(exports, "__esModule", { value: true }); |
|
exports.LeafLoadBalancer = exports.PickFirstLoadBalancer = exports.PickFirstLoadBalancingConfig = void 0; |
|
exports.shuffled = shuffled; |
|
exports.setup = setup; |
|
const load_balancer_1 = require("./load-balancer"); |
|
const connectivity_state_1 = require("./connectivity-state"); |
|
const picker_1 = require("./picker"); |
|
const subchannel_address_1 = require("./subchannel-address"); |
|
const logging = require("./logging"); |
|
const constants_1 = require("./constants"); |
|
const subchannel_address_2 = require("./subchannel-address"); |
|
const net_1 = require("net"); |
|
const TRACER_NAME = 'pick_first'; |
|
function trace(text) { |
|
logging.trace(constants_1.LogVerbosity.DEBUG, TRACER_NAME, text); |
|
} |
|
const TYPE_NAME = 'pick_first'; |
|
|
|
|
|
|
|
|
|
const CONNECTION_DELAY_INTERVAL_MS = 250; |
|
class PickFirstLoadBalancingConfig { |
|
constructor(shuffleAddressList) { |
|
this.shuffleAddressList = shuffleAddressList; |
|
} |
|
getLoadBalancerName() { |
|
return TYPE_NAME; |
|
} |
|
toJsonObject() { |
|
return { |
|
[TYPE_NAME]: { |
|
shuffleAddressList: this.shuffleAddressList, |
|
}, |
|
}; |
|
} |
|
getShuffleAddressList() { |
|
return this.shuffleAddressList; |
|
} |
|
|
|
static createFromJson(obj) { |
|
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); |
|
} |
|
} |
|
exports.PickFirstLoadBalancingConfig = PickFirstLoadBalancingConfig; |
|
|
|
|
|
|
|
|
|
class PickFirstPicker { |
|
constructor(subchannel) { |
|
this.subchannel = subchannel; |
|
} |
|
pick(pickArgs) { |
|
return { |
|
pickResultType: picker_1.PickResultType.COMPLETE, |
|
subchannel: this.subchannel, |
|
status: null, |
|
onCallStarted: null, |
|
onCallEnded: null, |
|
}; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function shuffled(list) { |
|
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; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function interleaveAddressFamilies(addressList) { |
|
if (addressList.length === 0) { |
|
return []; |
|
} |
|
const result = []; |
|
const ipv6Addresses = []; |
|
const ipv4Addresses = []; |
|
const ipv6First = (0, subchannel_address_2.isTcpSubchannelAddress)(addressList[0]) && (0, net_1.isIPv6)(addressList[0].host); |
|
for (const address of addressList) { |
|
if ((0, subchannel_address_2.isTcpSubchannelAddress)(address) && (0, net_1.isIPv6)(address.host)) { |
|
ipv6Addresses.push(address); |
|
} |
|
else { |
|
ipv4Addresses.push(address); |
|
} |
|
} |
|
const firstList = ipv6First ? ipv6Addresses : ipv4Addresses; |
|
const secondList = ipv6First ? ipv4Addresses : ipv6Addresses; |
|
for (let i = 0; i < Math.max(firstList.length, secondList.length); i++) { |
|
if (i < firstList.length) { |
|
result.push(firstList[i]); |
|
} |
|
if (i < secondList.length) { |
|
result.push(secondList[i]); |
|
} |
|
} |
|
return result; |
|
} |
|
const REPORT_HEALTH_STATUS_OPTION_NAME = 'grpc-node.internal.pick-first.report_health_status'; |
|
class PickFirstLoadBalancer { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
constructor(channelControlHelper) { |
|
this.channelControlHelper = channelControlHelper; |
|
|
|
|
|
|
|
|
|
this.children = []; |
|
|
|
|
|
|
|
this.currentState = connectivity_state_1.ConnectivityState.IDLE; |
|
|
|
|
|
|
|
|
|
this.currentSubchannelIndex = 0; |
|
|
|
|
|
|
|
|
|
|
|
this.currentPick = null; |
|
|
|
|
|
|
|
|
|
this.subchannelStateListener = (subchannel, previousState, newState, keepaliveTime, errorMessage) => { |
|
this.onSubchannelStateUpdate(subchannel, previousState, newState, errorMessage); |
|
}; |
|
this.pickedSubchannelHealthListener = () => this.calculateAndReportNewState(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
this.stickyTransientFailureMode = false; |
|
this.reportHealthStatus = false; |
|
|
|
|
|
|
|
|
|
this.lastError = null; |
|
this.latestAddressList = null; |
|
this.latestOptions = {}; |
|
this.connectionDelayTimeout = setTimeout(() => { }, 0); |
|
clearTimeout(this.connectionDelayTimeout); |
|
} |
|
allChildrenHaveReportedTF() { |
|
return this.children.every(child => child.hasReportedTransientFailure); |
|
} |
|
resetChildrenReportedTF() { |
|
this.children.every(child => child.hasReportedTransientFailure = false); |
|
} |
|
calculateAndReportNewState() { |
|
var _a; |
|
if (this.currentPick) { |
|
if (this.reportHealthStatus && !this.currentPick.isHealthy()) { |
|
const errorMessage = `Picked subchannel ${this.currentPick.getAddress()} is unhealthy`; |
|
this.updateState(connectivity_state_1.ConnectivityState.TRANSIENT_FAILURE, new picker_1.UnavailablePicker({ |
|
details: errorMessage, |
|
}), errorMessage); |
|
} |
|
else { |
|
this.updateState(connectivity_state_1.ConnectivityState.READY, new PickFirstPicker(this.currentPick), null); |
|
} |
|
} |
|
else if (((_a = this.latestAddressList) === null || _a === void 0 ? void 0 : _a.length) === 0) { |
|
const errorMessage = `No connection established. Last error: ${this.lastError}`; |
|
this.updateState(connectivity_state_1.ConnectivityState.TRANSIENT_FAILURE, new picker_1.UnavailablePicker({ |
|
details: errorMessage, |
|
}), errorMessage); |
|
} |
|
else if (this.children.length === 0) { |
|
this.updateState(connectivity_state_1.ConnectivityState.IDLE, new picker_1.QueuePicker(this), null); |
|
} |
|
else { |
|
if (this.stickyTransientFailureMode) { |
|
const errorMessage = `No connection established. Last error: ${this.lastError}`; |
|
this.updateState(connectivity_state_1.ConnectivityState.TRANSIENT_FAILURE, new picker_1.UnavailablePicker({ |
|
details: errorMessage, |
|
}), errorMessage); |
|
} |
|
else { |
|
this.updateState(connectivity_state_1.ConnectivityState.CONNECTING, new picker_1.QueuePicker(this), null); |
|
} |
|
} |
|
} |
|
requestReresolution() { |
|
this.channelControlHelper.requestReresolution(); |
|
} |
|
maybeEnterStickyTransientFailureMode() { |
|
if (!this.allChildrenHaveReportedTF()) { |
|
return; |
|
} |
|
this.requestReresolution(); |
|
this.resetChildrenReportedTF(); |
|
if (this.stickyTransientFailureMode) { |
|
this.calculateAndReportNewState(); |
|
return; |
|
} |
|
this.stickyTransientFailureMode = true; |
|
for (const { subchannel } of this.children) { |
|
subchannel.startConnecting(); |
|
} |
|
this.calculateAndReportNewState(); |
|
} |
|
removeCurrentPick() { |
|
if (this.currentPick !== null) { |
|
this.currentPick.removeConnectivityStateListener(this.subchannelStateListener); |
|
this.channelControlHelper.removeChannelzChild(this.currentPick.getChannelzRef()); |
|
this.currentPick.removeHealthStateWatcher(this.pickedSubchannelHealthListener); |
|
|
|
this.currentPick.unref(); |
|
this.currentPick = null; |
|
} |
|
} |
|
onSubchannelStateUpdate(subchannel, previousState, newState, errorMessage) { |
|
var _a; |
|
if ((_a = this.currentPick) === null || _a === void 0 ? void 0 : _a.realSubchannelEquals(subchannel)) { |
|
if (newState !== connectivity_state_1.ConnectivityState.READY) { |
|
this.removeCurrentPick(); |
|
this.calculateAndReportNewState(); |
|
} |
|
return; |
|
} |
|
for (const [index, child] of this.children.entries()) { |
|
if (subchannel.realSubchannelEquals(child.subchannel)) { |
|
if (newState === connectivity_state_1.ConnectivityState.READY) { |
|
this.pickSubchannel(child.subchannel); |
|
} |
|
if (newState === connectivity_state_1.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; |
|
} |
|
} |
|
} |
|
startNextSubchannelConnecting(startIndex) { |
|
clearTimeout(this.connectionDelayTimeout); |
|
for (const [index, child] of this.children.entries()) { |
|
if (index >= startIndex) { |
|
const subchannelState = child.subchannel.getConnectivityState(); |
|
if (subchannelState === connectivity_state_1.ConnectivityState.IDLE || |
|
subchannelState === connectivity_state_1.ConnectivityState.CONNECTING) { |
|
this.startConnecting(index); |
|
return; |
|
} |
|
} |
|
} |
|
this.maybeEnterStickyTransientFailureMode(); |
|
} |
|
|
|
|
|
|
|
|
|
startConnecting(subchannelIndex) { |
|
var _a, _b; |
|
clearTimeout(this.connectionDelayTimeout); |
|
this.currentSubchannelIndex = subchannelIndex; |
|
if (this.children[subchannelIndex].subchannel.getConnectivityState() === |
|
connectivity_state_1.ConnectivityState.IDLE) { |
|
trace('Start connecting to subchannel with address ' + |
|
this.children[subchannelIndex].subchannel.getAddress()); |
|
process.nextTick(() => { |
|
var _a; |
|
(_a = this.children[subchannelIndex]) === null || _a === void 0 ? void 0 : _a.subchannel.startConnecting(); |
|
}); |
|
} |
|
this.connectionDelayTimeout = setTimeout(() => { |
|
this.startNextSubchannelConnecting(subchannelIndex + 1); |
|
}, CONNECTION_DELAY_INTERVAL_MS); |
|
(_b = (_a = this.connectionDelayTimeout).unref) === null || _b === void 0 ? void 0 : _b.call(_a); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
pickSubchannel(subchannel) { |
|
trace('Pick subchannel with address ' + subchannel.getAddress()); |
|
this.stickyTransientFailureMode = false; |
|
|
|
|
|
subchannel.ref(); |
|
this.channelControlHelper.addChannelzChild(subchannel.getChannelzRef()); |
|
this.removeCurrentPick(); |
|
this.resetSubchannelList(); |
|
subchannel.addConnectivityStateListener(this.subchannelStateListener); |
|
subchannel.addHealthStateWatcher(this.pickedSubchannelHealthListener); |
|
this.currentPick = subchannel; |
|
clearTimeout(this.connectionDelayTimeout); |
|
this.calculateAndReportNewState(); |
|
} |
|
updateState(newState, picker, errorMessage) { |
|
trace(connectivity_state_1.ConnectivityState[this.currentState] + |
|
' -> ' + |
|
connectivity_state_1.ConnectivityState[newState]); |
|
this.currentState = newState; |
|
this.channelControlHelper.updateState(newState, picker, errorMessage); |
|
} |
|
resetSubchannelList() { |
|
for (const child of this.children) { |
|
|
|
|
|
child.subchannel.removeConnectivityStateListener(this.subchannelStateListener); |
|
|
|
|
|
|
|
|
|
child.subchannel.unref(); |
|
this.channelControlHelper.removeChannelzChild(child.subchannel.getChannelzRef()); |
|
} |
|
this.currentSubchannelIndex = 0; |
|
this.children = []; |
|
} |
|
connectToAddressList(addressList, options) { |
|
trace('connectToAddressList([' + addressList.map(address => (0, subchannel_address_1.subchannelAddressToString)(address)) + '])'); |
|
const newChildrenList = addressList.map(address => ({ |
|
subchannel: this.channelControlHelper.createSubchannel(address, options), |
|
hasReportedTransientFailure: false, |
|
})); |
|
for (const { subchannel } of newChildrenList) { |
|
if (subchannel.getConnectivityState() === connectivity_state_1.ConnectivityState.READY) { |
|
this.pickSubchannel(subchannel); |
|
return; |
|
} |
|
} |
|
|
|
|
|
|
|
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); |
|
} |
|
for (const child of this.children) { |
|
if (child.subchannel.getConnectivityState() === |
|
connectivity_state_1.ConnectivityState.TRANSIENT_FAILURE) { |
|
child.hasReportedTransientFailure = true; |
|
} |
|
} |
|
this.startNextSubchannelConnecting(0); |
|
this.calculateAndReportNewState(); |
|
} |
|
updateAddressList(endpointList, lbConfig, options) { |
|
if (!(lbConfig instanceof PickFirstLoadBalancingConfig)) { |
|
return; |
|
} |
|
this.reportHealthStatus = options[REPORT_HEALTH_STATUS_OPTION_NAME]; |
|
|
|
|
|
|
|
if (lbConfig.getShuffleAddressList()) { |
|
endpointList = shuffled(endpointList); |
|
} |
|
const rawAddressList = [].concat(...endpointList.map(endpoint => endpoint.addresses)); |
|
trace('updateAddressList([' + rawAddressList.map(address => (0, subchannel_address_1.subchannelAddressToString)(address)) + '])'); |
|
if (rawAddressList.length === 0) { |
|
this.lastError = 'No addresses resolved'; |
|
} |
|
const addressList = interleaveAddressFamilies(rawAddressList); |
|
this.latestAddressList = addressList; |
|
this.latestOptions = options; |
|
this.connectToAddressList(addressList, options); |
|
} |
|
exitIdle() { |
|
if (this.currentState === connectivity_state_1.ConnectivityState.IDLE && |
|
this.latestAddressList) { |
|
this.connectToAddressList(this.latestAddressList, this.latestOptions); |
|
} |
|
} |
|
resetBackoff() { |
|
|
|
|
|
} |
|
destroy() { |
|
this.resetSubchannelList(); |
|
this.removeCurrentPick(); |
|
} |
|
getTypeName() { |
|
return TYPE_NAME; |
|
} |
|
} |
|
exports.PickFirstLoadBalancer = PickFirstLoadBalancer; |
|
const LEAF_CONFIG = new PickFirstLoadBalancingConfig(false); |
|
|
|
|
|
|
|
|
|
|
|
class LeafLoadBalancer { |
|
constructor(endpoint, channelControlHelper, options) { |
|
this.endpoint = endpoint; |
|
this.options = options; |
|
this.latestState = connectivity_state_1.ConnectivityState.IDLE; |
|
const childChannelControlHelper = (0, load_balancer_1.createChildChannelControlHelper)(channelControlHelper, { |
|
updateState: (connectivityState, picker, errorMessage) => { |
|
this.latestState = connectivityState; |
|
this.latestPicker = picker; |
|
channelControlHelper.updateState(connectivityState, picker, errorMessage); |
|
}, |
|
}); |
|
this.pickFirstBalancer = new PickFirstLoadBalancer(childChannelControlHelper); |
|
this.latestPicker = new picker_1.QueuePicker(this.pickFirstBalancer); |
|
} |
|
startConnecting() { |
|
this.pickFirstBalancer.updateAddressList([this.endpoint], LEAF_CONFIG, Object.assign(Object.assign({}, this.options), { [REPORT_HEALTH_STATUS_OPTION_NAME]: true })); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
updateEndpoint(newEndpoint, newOptions) { |
|
this.options = newOptions; |
|
this.endpoint = newEndpoint; |
|
if (this.latestState !== connectivity_state_1.ConnectivityState.IDLE) { |
|
this.startConnecting(); |
|
} |
|
} |
|
getConnectivityState() { |
|
return this.latestState; |
|
} |
|
getPicker() { |
|
return this.latestPicker; |
|
} |
|
getEndpoint() { |
|
return this.endpoint; |
|
} |
|
exitIdle() { |
|
this.pickFirstBalancer.exitIdle(); |
|
} |
|
destroy() { |
|
this.pickFirstBalancer.destroy(); |
|
} |
|
} |
|
exports.LeafLoadBalancer = LeafLoadBalancer; |
|
function setup() { |
|
(0, load_balancer_1.registerLoadBalancerType)(TYPE_NAME, PickFirstLoadBalancer, PickFirstLoadBalancingConfig); |
|
(0, load_balancer_1.registerDefaultLoadBalancerType)(TYPE_NAME); |
|
} |
|
|