"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ResolvingLoadBalancer = void 0;
const load_balancer_1 = require("./load-balancer");
const service_config_1 = require("./service-config");
const connectivity_state_1 = require("./connectivity-state");
const resolver_1 = require("./resolver");
const picker_1 = require("./picker");
const backoff_timeout_1 = require("./backoff-timeout");
const constants_1 = require("./constants");
const metadata_1 = require("./metadata");
const logging = require("./logging");
const constants_2 = require("./constants");
const uri_parser_1 = require("./uri-parser");
const load_balancer_child_handler_1 = require("./load-balancer-child-handler");
const TRACER_NAME = 'resolving_load_balancer';
function trace(text) {
logging.trace(constants_2.LogVerbosity.DEBUG, TRACER_NAME, text);
}
const NAME_MATCH_LEVEL_ORDER = [
'SERVICE_AND_METHOD',
'SERVICE',
'EMPTY',
];
function hasMatchingName(service, method, methodConfig, matchLevel) {
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, method, methodConfigs, matchLevel) {
for (const config of methodConfigs) {
if (hasMatchingName(service, method, config, matchLevel)) {
return config;
}
}
return null;
}
function getDefaultConfigSelector(serviceConfig) {
return {
invoke(methodName, metadata) {
var _a, _b;
const splitName = methodName.split('/').filter(x => x.length > 0);
const service = (_a = splitName[0]) !== null && _a !== void 0 ? _a : '';
const method = (_b = splitName[1]) !== null && _b !== void 0 ? _b : '';
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: constants_1.Status.OK,
dynamicFilterFactories: [],
};
}
}
}
return {
methodConfig: { name: [] },
pickInformation: {},
status: constants_1.Status.OK,
dynamicFilterFactories: [],
};
},
unref() { }
};
}
class ResolvingLoadBalancer {
constructor(target, channelControlHelper, channelOptions, onSuccessfulResolution, onFailedResolution) {
this.target = target;
this.channelControlHelper = channelControlHelper;
this.channelOptions = channelOptions;
this.onSuccessfulResolution = onSuccessfulResolution;
this.onFailedResolution = onFailedResolution;
this.latestChildState = connectivity_state_1.ConnectivityState.IDLE;
this.latestChildPicker = new picker_1.QueuePicker(this);
this.latestChildErrorMessage = null;
this.currentState = connectivity_state_1.ConnectivityState.IDLE;
this.previousServiceConfig = null;
this.continueResolving = false;
if (channelOptions['grpc.service_config']) {
this.defaultServiceConfig = (0, service_config_1.validateServiceConfig)(JSON.parse(channelOptions['grpc.service_config']));
}
else {
this.defaultServiceConfig = {
loadBalancingConfig: [],
methodConfig: [],
};
}
this.updateState(connectivity_state_1.ConnectivityState.IDLE, new picker_1.QueuePicker(this), null);
this.childLoadBalancer = new load_balancer_child_handler_1.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, picker, errorMessage) => {
this.latestChildState = newState;
this.latestChildPicker = picker;
this.latestChildErrorMessage = errorMessage;
this.updateState(newState, picker, errorMessage);
},
addChannelzChild: channelControlHelper.addChannelzChild.bind(channelControlHelper),
removeChannelzChild: channelControlHelper.removeChannelzChild.bind(channelControlHelper),
});
this.innerResolver = (0, resolver_1.createResolver)(target, this.handleResolverResult.bind(this), channelOptions);
const backoffOptions = {
initialDelay: channelOptions['grpc.initial_reconnect_backoff_ms'],
maxDelay: channelOptions['grpc.max_reconnect_backoff_ms'],
};
this.backoffTimeout = new backoff_timeout_1.BackoffTimeout(() => {
if (this.continueResolving) {
this.updateResolution();
this.continueResolving = false;
}
else {
this.updateState(this.latestChildState, this.latestChildPicker, this.latestChildErrorMessage);
}
}, backoffOptions);
this.backoffTimeout.unref();
}
handleResolverResult(endpointList, attributes, serviceConfig, resolutionNote) {
var _a, _b;
this.backoffTimeout.stop();
this.backoffTimeout.reset();
let resultAccepted = true;
let workingServiceConfig = null;
if (serviceConfig === null) {
workingServiceConfig = this.defaultServiceConfig;
}
else if (serviceConfig.ok) {
workingServiceConfig = serviceConfig.value;
}
else {
if (this.previousServiceConfig !== null) {
workingServiceConfig = this.previousServiceConfig;
}
else {
resultAccepted = false;
this.handleResolutionFailure(serviceConfig.error);
}
}
if (workingServiceConfig !== null) {
const workingConfigList = (_a = workingServiceConfig === null || workingServiceConfig === void 0 ? void 0 : workingServiceConfig.loadBalancingConfig) !== null && _a !== void 0 ? _a : [];
const loadBalancingConfig = (0, load_balancer_1.selectLbConfigFromList)(workingConfigList, true);
if (loadBalancingConfig === null) {
resultAccepted = false;
this.handleResolutionFailure({
code: constants_1.Status.UNAVAILABLE,
details: 'All load balancer options in service config are not compatible',
metadata: new metadata_1.Metadata(),
});
}
else {
resultAccepted = this.childLoadBalancer.updateAddressList(endpointList, loadBalancingConfig, Object.assign(Object.assign({}, this.channelOptions), attributes), resolutionNote);
}
}
if (resultAccepted) {
this.onSuccessfulResolution(workingServiceConfig, (_b = attributes[resolver_1.CHANNEL_ARGS_CONFIG_SELECTOR_KEY]) !== null && _b !== void 0 ? _b : getDefaultConfigSelector(workingServiceConfig));
}
return resultAccepted;
}
updateResolution() {
this.innerResolver.updateResolution();
if (this.currentState === connectivity_state_1.ConnectivityState.IDLE) {
this.updateState(connectivity_state_1.ConnectivityState.CONNECTING, this.latestChildPicker, this.latestChildErrorMessage);
}
this.backoffTimeout.runOnce();
}
updateState(connectivityState, picker, errorMessage) {
trace((0, uri_parser_1.uriToString)(this.target) +
' ' +
connectivity_state_1.ConnectivityState[this.currentState] +
' -> ' +
connectivity_state_1.ConnectivityState[connectivityState]);
if (connectivityState === connectivity_state_1.ConnectivityState.IDLE) {
picker = new picker_1.QueuePicker(this, picker);
}
this.currentState = connectivityState;
this.channelControlHelper.updateState(connectivityState, picker, errorMessage);
}
handleResolutionFailure(error) {
if (this.latestChildState === connectivity_state_1.ConnectivityState.IDLE) {
this.updateState(connectivity_state_1.ConnectivityState.TRANSIENT_FAILURE, new picker_1.UnavailablePicker(error), error.details);
this.onFailedResolution(error);
}
}
exitIdle() {
if (this.currentState === connectivity_state_1.ConnectivityState.IDLE ||
this.currentState === connectivity_state_1.ConnectivityState.TRANSIENT_FAILURE) {
if (this.backoffTimeout.isRunning()) {
this.continueResolving = true;
}
else {
this.updateResolution();
}
}
this.childLoadBalancer.exitIdle();
}
updateAddressList(endpointList, lbConfig) {
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 = connectivity_state_1.ConnectivityState.IDLE;
this.latestChildPicker = new picker_1.QueuePicker(this);
this.currentState = connectivity_state_1.ConnectivityState.IDLE;
this.previousServiceConfig = null;
this.continueResolving = false;
}
getTypeName() {
return 'resolving_load_balancer';
}
}
exports.ResolvingLoadBalancer = ResolvingLoadBalancer;