"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 });
const load_balancer_1 = require("./load-balancer");
const channel_1 = require("./channel");
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 TRACER_NAME = 'resolving_load_balancer';
function trace(text) {
    logging.trace(constants_2.LogVerbosity.DEBUG, TRACER_NAME, text);
}
const DEFAULT_LOAD_BALANCER_NAME = 'pick_first';
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, defaultServiceConfig) {
        this.target = target;
        this.channelControlHelper = channelControlHelper;
        this.defaultServiceConfig = defaultServiceConfig;
        /**
         * Current internal load balancer used for handling calls.
         * Invariant: innerLoadBalancer === null => pendingReplacementLoadBalancer === null.
         */
        this.innerLoadBalancer = null;
        /**
         * The load balancer instance that will be used in place of the current
         * `innerLoadBalancer` once either that load balancer loses its connection
         * or this one establishes a connection. For use when a new name resolution
         * result comes in with a different load balancing configuration, and the
         * current `innerLoadBalancer` is still connected.
         */
        this.pendingReplacementLoadBalancer = null;
        /**
         * This resolving load balancer's current connectivity state.
         */
        this.currentState = channel_1.ConnectivityState.IDLE;
        /**
         * The service config object from the last successful resolution, if
         * available. A value of undefined indicates that there has not yet
         * been a successful resolution. A value of null indicates that the last
         * successful resolution explicitly provided a null service config.
         */
        this.previousServiceConfig = undefined;
        /**
         * The most recently reported connectivity state of the `innerLoadBalancer`.
         */
        this.innerBalancerState = channel_1.ConnectivityState.IDLE;
        this.innerBalancerPicker = new picker_1.UnavailablePicker();
        /**
         * The most recent reported state of the pendingReplacementLoadBalancer.
         * Starts at IDLE for type simplicity. This should get updated as soon as the
         * pendingReplacementLoadBalancer gets constructed.
         */
        this.replacementBalancerState = channel_1.ConnectivityState.IDLE;
        /**
         * The picker associated with the replacementBalancerState. Starts as an
         * UnavailablePicker for type simplicity. This should get updated as soon as
         * the pendingReplacementLoadBalancer gets constructed.
         */
        this.replacementBalancerPicker = new picker_1.UnavailablePicker();
        /**
         * Indicates whether we should attempt to resolve again after the backoff
         * timer runs out.
         */
        this.continueResolving = false;
        this.updateState(channel_1.ConnectivityState.IDLE, new picker_1.QueuePicker(this));
        this.innerResolver = resolver_1.createResolver(target, {
            onSuccessfulResolution: (addressList, serviceConfig, serviceConfigError) => {
                let workingServiceConfig = null;
                /* This first group of conditionals implements the algorithm described
                 * in https://github.com/grpc/proposal/blob/master/A21-service-config-error-handling.md
                 * in the section called "Behavior on receiving a new gRPC Config".
                 */
                if (serviceConfig === null) {
                    // Step 4 and 5
                    if (serviceConfigError === null) {
                        // Step 5
                        this.previousServiceConfig = serviceConfig;
                        workingServiceConfig = this.defaultServiceConfig;
                    }
                    else {
                        // Step 4
                        if (this.previousServiceConfig === undefined) {
                            // Step 4.ii
                            if (this.defaultServiceConfig === null) {
                                // Step 4.ii.b
                                this.handleResolutionFailure(serviceConfigError);
                            }
                            else {
                                // Step 4.ii.a
                                workingServiceConfig = this.defaultServiceConfig;
                            }
                        }
                        else {
                            // Step 4.i
                            workingServiceConfig = this.previousServiceConfig;
                        }
                    }
                }
                else {
                    // Step 3
                    workingServiceConfig = serviceConfig;
                    this.previousServiceConfig = serviceConfig;
                }
                let loadBalancerName = null;
                let loadBalancingConfig = null;
                if (workingServiceConfig === null ||
                    workingServiceConfig.loadBalancingConfig.length === 0) {
                    loadBalancerName = DEFAULT_LOAD_BALANCER_NAME;
                }
                else {
                    for (const lbConfig of workingServiceConfig.loadBalancingConfig) {
                        // Iterating through a oneof looking for whichever one is populated
                        for (const key in lbConfig) {
                            if (Object.prototype.hasOwnProperty.call(lbConfig, key)) {
                                if (load_balancer_1.isLoadBalancerNameRegistered(key)) {
                                    loadBalancerName = key;
                                    loadBalancingConfig = lbConfig;
                                    break;
                                }
                            }
                        }
                        if (loadBalancerName !== null) {
                            break;
                        }
                    }
                    if (loadBalancerName === null) {
                        // There were load balancing configs but none are supported. This counts as a resolution failure
                        this.handleResolutionFailure({
                            code: constants_1.Status.UNAVAILABLE,
                            details: 'All load balancer options in service config are not compatible',
                            metadata: new metadata_1.Metadata(),
                        });
                        return;
                    }
                }
                if (this.innerLoadBalancer === null) {
                    this.innerLoadBalancer = load_balancer_1.createLoadBalancer(loadBalancerName, this.innerChannelControlHelper);
                    this.innerLoadBalancer.updateAddressList(addressList, loadBalancingConfig);
                }
                else if (this.innerLoadBalancer.getTypeName() === loadBalancerName) {
                    this.innerLoadBalancer.updateAddressList(addressList, loadBalancingConfig);
                }
                else {
                    if (this.pendingReplacementLoadBalancer === null ||
                        this.pendingReplacementLoadBalancer.getTypeName() !==
                            loadBalancerName) {
                        if (this.pendingReplacementLoadBalancer !== null) {
                            this.pendingReplacementLoadBalancer.destroy();
                        }
                        this.pendingReplacementLoadBalancer = load_balancer_1.createLoadBalancer(loadBalancerName, this.replacementChannelControlHelper);
                    }
                    this.pendingReplacementLoadBalancer.updateAddressList(addressList, loadBalancingConfig);
                }
            },
            onError: (error) => {
                this.handleResolutionFailure(error);
            },
        });
        this.innerChannelControlHelper = {
            createSubchannel: (subchannelAddress, subchannelArgs) => {
                return this.channelControlHelper.createSubchannel(subchannelAddress, subchannelArgs);
            },
            updateState: (connectivityState, picker) => {
                this.innerBalancerState = connectivityState;
                if (connectivityState === channel_1.ConnectivityState.IDLE) {
                    picker = new picker_1.QueuePicker(this);
                }
                this.innerBalancerPicker = picker;
                if (connectivityState !== channel_1.ConnectivityState.READY &&
                    this.pendingReplacementLoadBalancer !== null) {
                    this.switchOverReplacementBalancer();
                }
                else {
                    if (connectivityState === channel_1.ConnectivityState.IDLE) {
                        if (this.innerLoadBalancer) {
                            this.innerLoadBalancer.destroy();
                            this.innerLoadBalancer = null;
                        }
                    }
                    this.updateState(connectivityState, picker);
                }
            },
            requestReresolution: () => {
                if (this.pendingReplacementLoadBalancer === null) {
                    /* 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()) {
                        this.continueResolving = true;
                    }
                    else {
                        this.updateResolution();
                    }
                }
            },
        };
        this.replacementChannelControlHelper = {
            createSubchannel: (subchannelAddress, subchannelArgs) => {
                return this.channelControlHelper.createSubchannel(subchannelAddress, subchannelArgs);
            },
            updateState: (connectivityState, picker) => {
                if (connectivityState === channel_1.ConnectivityState.IDLE) {
                    picker = new picker_1.QueuePicker(this);
                }
                this.replacementBalancerState = connectivityState;
                this.replacementBalancerPicker = picker;
                if (connectivityState === channel_1.ConnectivityState.READY) {
                    this.switchOverReplacementBalancer();
                }
                else if (connectivityState === channel_1.ConnectivityState.IDLE) {
                    if (this.pendingReplacementLoadBalancer) {
                        this.pendingReplacementLoadBalancer.destroy();
                        this.pendingReplacementLoadBalancer = null;
                    }
                }
            },
            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()) {
                    this.continueResolving = true;
                }
                else {
                    this.updateResolution();
                }
            },
        };
        this.backoffTimeout = new backoff_timeout_1.BackoffTimeout(() => {
            if (this.continueResolving) {
                this.updateResolution();
                this.continueResolving = false;
            }
            else {
                if (this.innerLoadBalancer === null) {
                    this.updateState(channel_1.ConnectivityState.IDLE, new picker_1.QueuePicker(this));
                }
                else {
                    this.updateState(this.innerBalancerState, this.innerBalancerPicker);
                }
            }
        });
    }
    updateResolution() {
        this.innerResolver.updateResolution();
        if (this.innerLoadBalancer === null || this.innerBalancerState === channel_1.ConnectivityState.IDLE) {
            this.updateState(channel_1.ConnectivityState.CONNECTING, new picker_1.QueuePicker(this));
        }
    }
    updateState(connectivitystate, picker) {
        trace(this.target + ' ' + channel_1.ConnectivityState[this.currentState] + ' -> ' + channel_1.ConnectivityState[connectivitystate]);
        this.currentState = connectivitystate;
        this.channelControlHelper.updateState(connectivitystate, picker);
    }
    /**
     * Stop using the current innerLoadBalancer and replace it with the
     * pendingReplacementLoadBalancer. Must only be called if both of
     * those are currently not null.
     */
    switchOverReplacementBalancer() {
        this.innerLoadBalancer.destroy();
        this.innerLoadBalancer = this.pendingReplacementLoadBalancer;
        this.innerLoadBalancer.replaceChannelControlHelper(this.innerChannelControlHelper);
        this.pendingReplacementLoadBalancer = null;
        this.innerBalancerState = this.replacementBalancerState;
        this.innerBalancerPicker = this.replacementBalancerPicker;
        this.updateState(this.replacementBalancerState, this.replacementBalancerPicker);
    }
    handleResolutionFailure(error) {
        if (this.innerLoadBalancer === null || this.innerBalancerState === channel_1.ConnectivityState.IDLE) {
            this.updateState(channel_1.ConnectivityState.TRANSIENT_FAILURE, new picker_1.UnavailablePicker(error));
        }
        this.backoffTimeout.runOnce();
    }
    exitIdle() {
        if (this.innerLoadBalancer !== null) {
            this.innerLoadBalancer.exitIdle();
        }
        if (this.currentState === channel_1.ConnectivityState.IDLE) {
            if (this.backoffTimeout.isRunning()) {
                this.continueResolving = true;
            }
            else {
                this.updateResolution();
            }
            this.updateState(channel_1.ConnectivityState.CONNECTING, new picker_1.QueuePicker(this));
        }
    }
    updateAddressList(addressList, lbConfig) {
        throw new Error('updateAddressList not supported on ResolvingLoadBalancer');
    }
    resetBackoff() {
        this.backoffTimeout.reset();
        if (this.innerLoadBalancer !== null) {
            this.innerLoadBalancer.resetBackoff();
        }
        if (this.pendingReplacementLoadBalancer !== null) {
            this.pendingReplacementLoadBalancer.resetBackoff();
        }
    }
    destroy() {
        if (this.innerLoadBalancer !== null) {
            this.innerLoadBalancer.destroy();
            this.innerLoadBalancer = null;
        }
        if (this.pendingReplacementLoadBalancer !== null) {
            this.pendingReplacementLoadBalancer.destroy();
            this.pendingReplacementLoadBalancer = null;
        }
        this.updateState(channel_1.ConnectivityState.SHUTDOWN, new picker_1.UnavailablePicker());
    }
    getTypeName() {
        return 'resolving_load_balancer';
    }
    replaceChannelControlHelper(channelControlHelper) {
        this.channelControlHelper = channelControlHelper;
    }
}
exports.ResolvingLoadBalancer = ResolvingLoadBalancer;
//# sourceMappingURL=resolving-load-balancer.js.map