// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import { __awaiter, __extends, __generator } from "tslib";
import { AbortError } from "@azure/abort-controller";
import { BaseRequestPolicy } from "@azure/core-http";
import { URLConstants } from "../utils/constants";
import { delay, setURLHost, setURLParameter } from "../utils/utils.common";
import { logger } from "../log";
/**
 * A factory method used to generated a RetryPolicy factory.
 *
 * @param retryOptions -
 */
export function NewRetryPolicyFactory(retryOptions) {
    return {
        create: function (nextPolicy, options) {
            return new StorageRetryPolicy(nextPolicy, options, retryOptions);
        }
    };
}
/**
 * RetryPolicy types.
 */
export var StorageRetryPolicyType;
(function (StorageRetryPolicyType) {
    /**
     * Exponential retry. Retry time delay grows exponentially.
     */
    StorageRetryPolicyType[StorageRetryPolicyType["EXPONENTIAL"] = 0] = "EXPONENTIAL";
    /**
     * Linear retry. Retry time delay grows linearly.
     */
    StorageRetryPolicyType[StorageRetryPolicyType["FIXED"] = 1] = "FIXED";
})(StorageRetryPolicyType || (StorageRetryPolicyType = {}));
// Default values of StorageRetryOptions
var DEFAULT_RETRY_OPTIONS = {
    maxRetryDelayInMs: 120 * 1000,
    maxTries: 4,
    retryDelayInMs: 4 * 1000,
    retryPolicyType: StorageRetryPolicyType.EXPONENTIAL,
    secondaryHost: "",
    tryTimeoutInMs: undefined // Use server side default timeout strategy
};
var RETRY_ABORT_ERROR = new AbortError("The operation was aborted.");
/**
 * Retry policy with exponential retry and linear retry implemented.
 */
var StorageRetryPolicy = /** @class */ (function (_super) {
    __extends(StorageRetryPolicy, _super);
    /**
     * Creates an instance of RetryPolicy.
     *
     * @param nextPolicy -
     * @param options -
     * @param retryOptions -
     */
    function StorageRetryPolicy(nextPolicy, options, retryOptions) {
        if (retryOptions === void 0) { retryOptions = DEFAULT_RETRY_OPTIONS; }
        var _this = _super.call(this, nextPolicy, options) || this;
        // Initialize retry options
        _this.retryOptions = {
            retryPolicyType: retryOptions.retryPolicyType
                ? retryOptions.retryPolicyType
                : DEFAULT_RETRY_OPTIONS.retryPolicyType,
            maxTries: retryOptions.maxTries && retryOptions.maxTries >= 1
                ? Math.floor(retryOptions.maxTries)
                : DEFAULT_RETRY_OPTIONS.maxTries,
            tryTimeoutInMs: retryOptions.tryTimeoutInMs && retryOptions.tryTimeoutInMs >= 0
                ? retryOptions.tryTimeoutInMs
                : DEFAULT_RETRY_OPTIONS.tryTimeoutInMs,
            retryDelayInMs: retryOptions.retryDelayInMs && retryOptions.retryDelayInMs >= 0
                ? Math.min(retryOptions.retryDelayInMs, retryOptions.maxRetryDelayInMs
                    ? retryOptions.maxRetryDelayInMs
                    : DEFAULT_RETRY_OPTIONS.maxRetryDelayInMs)
                : DEFAULT_RETRY_OPTIONS.retryDelayInMs,
            maxRetryDelayInMs: retryOptions.maxRetryDelayInMs && retryOptions.maxRetryDelayInMs >= 0
                ? retryOptions.maxRetryDelayInMs
                : DEFAULT_RETRY_OPTIONS.maxRetryDelayInMs,
            secondaryHost: retryOptions.secondaryHost
                ? retryOptions.secondaryHost
                : DEFAULT_RETRY_OPTIONS.secondaryHost
        };
        return _this;
    }
    /**
     * Sends request.
     *
     * @param request -
     */
    StorageRetryPolicy.prototype.sendRequest = function (request) {
        return __awaiter(this, void 0, void 0, function () {
            return __generator(this, function (_a) {
                return [2 /*return*/, this.attemptSendRequest(request, false, 1)];
            });
        });
    };
    /**
     * Decide and perform next retry. Won't mutate request parameter.
     *
     * @param request -
     * @param secondaryHas404 -  If attempt was against the secondary & it returned a StatusNotFound (404), then
     *                                   the resource was not found. This may be due to replication delay. So, in this
     *                                   case, we'll never try the secondary again for this operation.
     * @param attempt -           How many retries has been attempted to performed, starting from 1, which includes
     *                                   the attempt will be performed by this method call.
     */
    StorageRetryPolicy.prototype.attemptSendRequest = function (request, secondaryHas404, attempt) {
        return __awaiter(this, void 0, void 0, function () {
            var newRequest, isPrimaryRetry, response, err_1;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        newRequest = request.clone();
                        isPrimaryRetry = secondaryHas404 ||
                            !this.retryOptions.secondaryHost ||
                            !(request.method === "GET" || request.method === "HEAD" || request.method === "OPTIONS") ||
                            attempt % 2 === 1;
                        if (!isPrimaryRetry) {
                            newRequest.url = setURLHost(newRequest.url, this.retryOptions.secondaryHost);
                        }
                        // Set the server-side timeout query parameter "timeout=[seconds]"
                        if (this.retryOptions.tryTimeoutInMs) {
                            newRequest.url = setURLParameter(newRequest.url, URLConstants.Parameters.TIMEOUT, Math.floor(this.retryOptions.tryTimeoutInMs / 1000).toString());
                        }
                        _a.label = 1;
                    case 1:
                        _a.trys.push([1, 3, , 4]);
                        logger.info("RetryPolicy: =====> Try=" + attempt + " " + (isPrimaryRetry ? "Primary" : "Secondary"));
                        return [4 /*yield*/, this._nextPolicy.sendRequest(newRequest)];
                    case 2:
                        response = _a.sent();
                        if (!this.shouldRetry(isPrimaryRetry, attempt, response)) {
                            return [2 /*return*/, response];
                        }
                        secondaryHas404 = secondaryHas404 || (!isPrimaryRetry && response.status === 404);
                        return [3 /*break*/, 4];
                    case 3:
                        err_1 = _a.sent();
                        logger.error("RetryPolicy: Caught error, message: " + err_1.message + ", code: " + err_1.code);
                        if (!this.shouldRetry(isPrimaryRetry, attempt, response, err_1)) {
                            throw err_1;
                        }
                        return [3 /*break*/, 4];
                    case 4: return [4 /*yield*/, this.delay(isPrimaryRetry, attempt, request.abortSignal)];
                    case 5:
                        _a.sent();
                        return [4 /*yield*/, this.attemptSendRequest(request, secondaryHas404, ++attempt)];
                    case 6: return [2 /*return*/, _a.sent()];
                }
            });
        });
    };
    /**
     * Decide whether to retry according to last HTTP response and retry counters.
     *
     * @param isPrimaryRetry -
     * @param attempt -
     * @param response -
     * @param err -
     */
    StorageRetryPolicy.prototype.shouldRetry = function (isPrimaryRetry, attempt, response, err) {
        if (attempt >= this.retryOptions.maxTries) {
            logger.info("RetryPolicy: Attempt(s) " + attempt + " >= maxTries " + this.retryOptions
                .maxTries + ", no further try.");
            return false;
        }
        // Handle network failures, you may need to customize the list when you implement
        // your own http client
        var retriableErrors = [
            "ETIMEDOUT",
            "ESOCKETTIMEDOUT",
            "ECONNREFUSED",
            "ECONNRESET",
            "ENOENT",
            "ENOTFOUND",
            "TIMEOUT",
            "EPIPE",
            "REQUEST_SEND_ERROR" // For default xhr based http client provided in ms-rest-js
        ];
        if (err) {
            for (var _i = 0, retriableErrors_1 = retriableErrors; _i < retriableErrors_1.length; _i++) {
                var retriableError = retriableErrors_1[_i];
                if (err.name.toUpperCase().includes(retriableError) ||
                    err.message.toUpperCase().includes(retriableError) ||
                    (err.code && err.code.toString().toUpperCase() === retriableError)) {
                    logger.info("RetryPolicy: Network error " + retriableError + " found, will retry.");
                    return true;
                }
            }
        }
        // If attempt was against the secondary & it returned a StatusNotFound (404), then
        // the resource was not found. This may be due to replication delay. So, in this
        // case, we'll never try the secondary again for this operation.
        if (response || err) {
            var statusCode = response ? response.status : err ? err.statusCode : 0;
            if (!isPrimaryRetry && statusCode === 404) {
                logger.info("RetryPolicy: Secondary access with 404, will retry.");
                return true;
            }
            // Server internal error or server timeout
            if (statusCode === 503 || statusCode === 500) {
                logger.info("RetryPolicy: Will retry for status code " + statusCode + ".");
                return true;
            }
        }
        if ((err === null || err === void 0 ? void 0 : err.code) === "PARSE_ERROR" && (err === null || err === void 0 ? void 0 : err.message.startsWith("Error \"Error: Unclosed root tag"))) {
            logger.info("RetryPolicy: Incomplete XML response likely due to service timeout, will retry.");
            return true;
        }
        return false;
    };
    /**
     * Delay a calculated time between retries.
     *
     * @param isPrimaryRetry -
     * @param attempt -
     * @param abortSignal -
     */
    StorageRetryPolicy.prototype.delay = function (isPrimaryRetry, attempt, abortSignal) {
        return __awaiter(this, void 0, void 0, function () {
            var delayTimeInMs;
            return __generator(this, function (_a) {
                delayTimeInMs = 0;
                if (isPrimaryRetry) {
                    switch (this.retryOptions.retryPolicyType) {
                        case StorageRetryPolicyType.EXPONENTIAL:
                            delayTimeInMs = Math.min((Math.pow(2, attempt - 1) - 1) * this.retryOptions.retryDelayInMs, this.retryOptions.maxRetryDelayInMs);
                            break;
                        case StorageRetryPolicyType.FIXED:
                            delayTimeInMs = this.retryOptions.retryDelayInMs;
                            break;
                    }
                }
                else {
                    delayTimeInMs = Math.random() * 1000;
                }
                logger.info("RetryPolicy: Delay for " + delayTimeInMs + "ms");
                return [2 /*return*/, delay(delayTimeInMs, abortSignal, RETRY_ABORT_ERROR)];
            });
        });
    };
    return StorageRetryPolicy;
}(BaseRequestPolicy));
export { StorageRetryPolicy };
//# sourceMappingURL=StorageRetryPolicy.js.map