"use strict";
/*
 * Licensed to Elasticsearch B.V. under one or more contributor
 * license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright
 * ownership. Elasticsearch B.V. licenses this file to you 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 tslib_1 = require("tslib");
const BaseConnectionPool_1 = tslib_1.__importDefault(require("./BaseConnectionPool"));
const node_assert_1 = tslib_1.__importDefault(require("node:assert"));
const debug_1 = tslib_1.__importDefault(require("debug"));
const connection_1 = require("../connection");
const debug = (0, debug_1.default)('elasticsearch');
class ClusterConnectionPool extends BaseConnectionPool_1.default {
    constructor(opts) {
        var _a, _b;
        super(opts);
        Object.defineProperty(this, "dead", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        Object.defineProperty(this, "resurrectTimeout", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        Object.defineProperty(this, "resurrectTimeoutCutoff", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        Object.defineProperty(this, "pingTimeout", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        Object.defineProperty(this, "resurrectStrategy", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        this.dead = [];
        // the resurrect timeout is 60s
        this.resurrectTimeout = 1000 * 60;
        // number of consecutive failures after which
        // the timeout doesn't increase
        this.resurrectTimeoutCutoff = 5;
        this.pingTimeout = (_a = opts.pingTimeout) !== null && _a !== void 0 ? _a : 3000;
        const resurrectStrategy = (_b = opts.resurrectStrategy) !== null && _b !== void 0 ? _b : 'ping';
        this.resurrectStrategy = ClusterConnectionPool.resurrectStrategies[resurrectStrategy];
        (0, node_assert_1.default)(this.resurrectStrategy != null, `Invalid resurrection strategy: '${resurrectStrategy}'`);
    }
    /**
     * Marks a connection as 'alive'.
     * If needed removes the connection from the dead list
     * and then resets the `deadCount`.
     *
     * @param {object} connection
     */
    markAlive(connection) {
        const { id } = connection;
        debug(`Marking as 'alive' connection '${id}'`);
        const index = this.dead.indexOf(id);
        if (index > -1)
            this.dead.splice(index, 1);
        connection.status = connection_1.BaseConnection.statuses.ALIVE;
        connection.deadCount = 0;
        connection.resurrectTimeout = 0;
        return this;
    }
    /**
     * Marks a connection as 'dead'.
     * If needed adds the connection to the dead list
     * and then increments the `deadCount`.
     *
     * @param {object} connection
     */
    markDead(connection) {
        const { id } = connection;
        debug(`Marking as 'dead' connection '${id}'`);
        if (!this.dead.includes(id)) {
            // It might happen that `markDead` is called jsut after
            // a pool update, and in such case we will add to the dead
            // list a node that no longer exist. The following check verify
            // that the connection is still part of the pool before
            // marking it as dead.
            for (let i = 0; i < this.size; i++) {
                if (this.connections[i].id === id) {
                    this.dead.push(id);
                    break;
                }
            }
        }
        connection.status = connection_1.BaseConnection.statuses.DEAD;
        connection.deadCount++;
        // resurrectTimeout formula:
        // `resurrectTimeout * 2 ** min(deadCount - 1, resurrectTimeoutCutoff)`
        connection.resurrectTimeout = Date.now() + this.resurrectTimeout * Math.pow(2, Math.min(connection.deadCount - 1, this.resurrectTimeoutCutoff));
        // sort the dead list in ascending order
        // based on the resurrectTimeout
        this.dead.sort((a, b) => {
            const conn1 = this.connections.find(c => c.id === a);
            const conn2 = this.connections.find(c => c.id === b);
            return conn1.resurrectTimeout - conn2.resurrectTimeout;
        });
        return this;
    }
    /**
     * If enabled, tries to resurrect a connection with the given
     * resurrect strategy ('ping', 'optimistic', 'none').
     *
     * @param {object} { now, requestId }
     */
    resurrect(opts) {
        if (this.resurrectStrategy === 0 || this.dead.length === 0) {
            debug('Nothing to resurrect');
            return;
        }
        // the dead list is sorted in ascending order based on the timeout
        // so the first element will always be the one with the smaller timeout
        const connection = this.connections.find(c => c.id === this.dead[0]);
        if (opts.now < connection.resurrectTimeout) {
            debug('Nothing to resurrect');
            return;
        }
        const { id } = connection;
        // ping strategy
        if (this.resurrectStrategy === 1) {
            connection.request({ method: 'HEAD', path: '/' }, { timeout: this.pingTimeout, requestId: opts.requestId, name: opts.name, context: opts.context })
                .then(({ statusCode }) => {
                let isAlive = true;
                if (statusCode === 502 || statusCode === 503 || statusCode === 504) {
                    debug(`Resurrect: connection '${id}' is still dead`);
                    this.markDead(connection);
                    isAlive = false;
                }
                else {
                    debug(`Resurrect: connection '${id}' is now alive`);
                    this.markAlive(connection);
                }
                this.diagnostic.emit('resurrect', null, {
                    strategy: 'ping',
                    name: opts.name,
                    request: { id: opts.requestId },
                    isAlive,
                    connection
                });
            })
                .catch((err) => {
                this.markDead(connection);
                this.diagnostic.emit('resurrect', err, {
                    strategy: 'ping',
                    name: opts.name,
                    request: { id: opts.requestId },
                    isAlive: false,
                    connection
                });
            });
            // optimistic strategy
        }
        else {
            debug(`Resurrect: optimistic resurrection for connection '${id}'`);
            this.dead.splice(this.dead.indexOf(id), 1);
            connection.status = connection_1.BaseConnection.statuses.ALIVE;
            this.diagnostic.emit('resurrect', null, {
                strategy: 'optimistic',
                name: opts.name,
                request: { id: opts.requestId },
                isAlive: true,
                connection
            });
        }
    }
    /**
     * Returns an alive connection if present,
     * otherwise returns a dead connection.
     * By default it filters the `master` only nodes.
     * It uses the selector to choose which
     * connection return.
     *
     * @param {object} options (filter and selector)
     * @returns {object|null} connection
     */
    getConnection(opts) {
        const filter = opts.filter != null ? opts.filter : () => true;
        const selector = opts.selector != null ? opts.selector : (c) => c[0];
        this.resurrect({
            now: opts.now,
            requestId: opts.requestId,
            name: opts.name,
            context: opts.context
        });
        const noAliveConnections = this.size === this.dead.length;
        // TODO: can we cache this?
        const connections = [];
        for (let i = 0; i < this.size; i++) {
            const connection = this.connections[i];
            if (noAliveConnections || connection.status === connection_1.BaseConnection.statuses.ALIVE) {
                if (filter(connection)) {
                    connections.push(connection);
                }
            }
        }
        if (connections.length === 0)
            return null;
        return selector(connections);
    }
    /**
     * Empties the connection pool.
     *
     * @returns {ConnectionPool}
     */
    async empty() {
        await super.empty();
        this.dead = [];
    }
    /**
     * Update the ConnectionPool with new connections.
     *
     * @param {array} array of connections
     * @returns {ConnectionPool}
     */
    update(connections) {
        super.update(connections);
        this.dead = [];
        return this;
    }
}
Object.defineProperty(ClusterConnectionPool, "resurrectStrategies", {
    enumerable: true,
    configurable: true,
    writable: true,
    value: {
        none: 0,
        ping: 1,
        optimistic: 2
    }
});
exports.default = ClusterConnectionPool;
//# sourceMappingURL=ClusterConnectionPool.js.map