"use strict";
/*
 * Copyright 2019 gRPC authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */
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);
}
/**
 * Name match levels in order from most to least specific. This is the order in
 * which searches will be performed.
 */
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) {
                /* Check for the following in order, and return the first method
                * config that matches:
                * 1. A name that exactly matches the service and method
                * 2. A name with no method set that matches the service
                * 3. An empty name
                */
                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 {
    /**
     * Wrapper class that behaves like a `LoadBalancer` and also handles name
     * resolution internally.
     * @param target The address of the backend to connect to.
     * @param channelControlHelper `ChannelControlHelper` instance provided by
     *     this load balancer's owner.
     * @param defaultServiceConfig The default service configuration to be used
     *     if none is provided by the name resolver. A `null` value indicates
     *     that the default behavior should be the default unconfigured behavior.
     *     In practice, that means using the "pick first" load balancer
     *     implmentation
     */
    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 resolving load balancer's current connectivity state.
         */
        this.currentState = connectivity_state_1.ConnectivityState.IDLE;
        /**
         * The service config object from the last successful resolution, if
         * available. A value of null indicates that we have not yet received a valid
         * service config from the resolver.
         */
        this.previousServiceConfig = null;
        /**
         * Indicates whether we should attempt to resolve again after the backoff
         * timer runs out.
         */
        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 the backoffTimeout is running, we're still backing off from
                 * making resolve requests, so we shouldn't make another one here.
                 * In that case, the backoff timer callback will call
                 * updateResolution */
                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.latestChildPicker is initialized as new QueuePicker(this), which
             * is an appropriate value here if the child LB policy is unset.
             * Otherwise, we want to delegate to the child here, in case that
             * triggers something. */
            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]);
        // Ensure that this.exitIdle() is called by the picker
        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;
//# sourceMappingURL=resolving-load-balancer.js.map