"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.SummaryGenerator = exports.RetriableSummaryError = exports.SummarizeResultBuilder = exports.getFailMessage = exports.raceTimer = void 0;
const internal_1 = require("@fluidframework/core-utils/internal");
const internal_2 = require("@fluidframework/driver-definitions/internal");
const internal_3 = require("@fluidframework/driver-utils/internal");
const internal_4 = require("@fluidframework/telemetry-utils/internal");
/** Helper function to wait for a promise or PromiseTimer to elapse. */
async function raceTimer(promise, timer, cancellationToken) {
    const promises = [
        promise.then((value) => ({ result: "done", value })),
        timer.then(({ timerResult: result }) => ({ result })),
    ];
    if (cancellationToken !== undefined) {
        promises.push(cancellationToken.waitCancelled.then(() => ({ result: "cancelled" })));
    }
    return Promise.race(promises);
}
exports.raceTimer = raceTimer;
// Send some telemetry if generate summary takes too long
const maxSummarizeTimeoutTime = 20000; // 20 sec
const maxSummarizeTimeoutCount = 5; // Double and resend 5 times
const summarizeErrors = {
    /**
     * Error encountered while generating the summary tree, uploading
     * it to storage, or submitting the op. It could be a result of
     * the client becoming disconnected while generating or an actual error.
     */
    submitSummaryFailure: "Error while generating, uploading, or submitting summary",
    /**
     * The summaryAckWaitTimeout time has elapsed before receiving the summarize op
     * sent by this summarize attempt. It is expected to be broadcast quickly.
     */
    summaryOpWaitTimeout: "Timeout while waiting for summarize op broadcast",
    /**
     * The summaryAckWaitTimeout time has elapsed before receiving either a
     * summaryAck or summaryNack op from the server in response to this
     * summarize attempt. It is expected that the server should respond.
     */
    summaryAckWaitTimeout: "Timeout while waiting for summaryAck/summaryNack op",
    /**
     * The server responded with a summaryNack op, thus rejecting this
     * summarize attempt.
     */
    summaryNack: "Server rejected summary via summaryNack op",
    disconnect: "Summary cancelled due to summarizer or main client disconnect",
};
// Helper functions to report failures and return.
const getFailMessage = (errorCode) => `${errorCode}: ${summarizeErrors[errorCode]}`;
exports.getFailMessage = getFailMessage;
class SummarizeResultBuilder {
    constructor() {
        this.summarySubmitted = new internal_1.Deferred();
        this.summaryOpBroadcasted = new internal_1.Deferred();
        this.receivedSummaryAckOrNack = new internal_1.Deferred();
    }
    /**
     * Fails one or more of the three results as per the passed params.
     * If submit fails, all three results fail.
     * If op broadcast fails, only op broadcast result and ack nack result fails.
     * If ack nack fails, only ack nack result fails.
     */
    fail(message, error, submitFailureResult, nackSummaryResult) {
        (0, internal_1.assert)(!this.receivedSummaryAckOrNack.isCompleted, 0x25e /* "no reason to call fail if all promises have been completed" */);
        const result = {
            success: false,
            message,
            data: undefined,
            error,
        };
        // Note that if any of these are already resolved, it will be a no-op. For example, if ack nack failed but
        // submit summary and op broadcast has already been resolved as passed, only ack nack result will get modified.
        this.summarySubmitted.resolve({ ...result, data: submitFailureResult });
        this.summaryOpBroadcasted.resolve(result);
        this.receivedSummaryAckOrNack.resolve({ ...result, data: nackSummaryResult });
    }
    build() {
        return {
            summarySubmitted: this.summarySubmitted.promise,
            summaryOpBroadcasted: this.summaryOpBroadcasted.promise,
            receivedSummaryAckOrNack: this.receivedSummaryAckOrNack.promise,
        };
    }
}
exports.SummarizeResultBuilder = SummarizeResultBuilder;
/**
 * Errors type for errors hit during summary that may be retriable.
 */
class RetriableSummaryError extends internal_4.LoggingError {
    constructor(message, retryAfterSeconds, props) {
        super(message, props);
        this.retryAfterSeconds = retryAfterSeconds;
    }
}
exports.RetriableSummaryError = RetriableSummaryError;
/**
 * This class generates and tracks a summary attempt.
 */
class SummaryGenerator {
    constructor(pendingAckTimer, heuristicData, submitSummaryCallback, successfulSummaryCallback, refreshLatestSummaryCallback, summaryWatcher, logger) {
        this.pendingAckTimer = pendingAckTimer;
        this.heuristicData = heuristicData;
        this.submitSummaryCallback = submitSummaryCallback;
        this.successfulSummaryCallback = successfulSummaryCallback;
        this.refreshLatestSummaryCallback = refreshLatestSummaryCallback;
        this.summaryWatcher = summaryWatcher;
        this.logger = logger;
        this.summarizeTimer = new internal_1.Timer(maxSummarizeTimeoutTime, () => this.summarizeTimerHandler(maxSummarizeTimeoutTime, 1));
    }
    /**
     * Generates summary and listens for broadcast and ack/nack.
     * Returns true for ack, false for nack, and undefined for failure or timeout.
     * @param summaryOptions - options controlling how the summary is generated or submitted.
     * @param resultsBuilder - optional, result builder to use to build pass or fail result.
     */
    summarize(summaryOptions, resultsBuilder = new SummarizeResultBuilder()) {
        this.summarizeCore(summaryOptions, resultsBuilder).catch((error) => {
            const message = "UnexpectedSummarizeError";
            summaryOptions.summaryLogger.sendErrorEvent({ eventName: message }, error);
            resultsBuilder.fail(message, error);
        });
        return resultsBuilder.build();
    }
    async summarizeCore(submitSummaryOptions, resultsBuilder) {
        const { summaryLogger, cancellationToken, ...summarizeOptions } = submitSummaryOptions;
        // Note: timeSinceLastAttempt and timeSinceLastSummary for the
        // first summary are basically the time since the summarizer was loaded.
        const timeSinceLastAttempt = Date.now() - this.heuristicData.lastAttempt.summaryTime;
        const timeSinceLastSummary = Date.now() - this.heuristicData.lastSuccessfulSummary.summaryTime;
        let summarizeTelemetryProps = {
            ...summarizeOptions,
            fullTree: summarizeOptions.fullTree ?? false,
            timeSinceLastAttempt,
            timeSinceLastSummary,
        };
        const summarizeEvent = internal_4.PerformanceEvent.start(summaryLogger, {
            eventName: "Summarize",
            ...summarizeTelemetryProps,
        }, { start: true, end: true, cancel: "generic" });
        let summaryData;
        /**
         * Summarization can fail during submit, during op broadcast or during nack.
         * For submit failures, submitFailureResult should be provided. For nack failures, nackSummaryResult should
         * be provided. For op broadcast failures, only errors / properties should be provided.
         */
        const fail = (errorCode, error, properties, submitFailureResult, nackSummaryResult) => {
            // Report any failure as an error unless it was due to cancellation (like "disconnected" error)
            // If failure happened on upload, we may not yet realized that socket disconnected, so check
            // offlineError too.
            const category = cancellationToken.cancelled ||
                ((0, internal_4.isFluidError)(error) && error?.errorType === internal_2.DriverErrorTypes.offlineError)
                ? "generic"
                : "error";
            const reason = (0, exports.getFailMessage)(errorCode);
            summarizeEvent.cancel({
                ...properties,
                reason,
                category,
                retryAfterSeconds: error.retryAfterSeconds,
            }, error); // disconnect & summaryAckTimeout do not have proper error.
            resultsBuilder.fail(reason, error, submitFailureResult, nackSummaryResult);
        };
        // Wait to generate and send summary
        this.summarizeTimer.start();
        try {
            // Need to save refSeqNum before we record new attempt (happens as part of submitSummaryCallback)
            const lastAttemptRefSeqNum = this.heuristicData.lastAttempt.refSequenceNumber;
            summaryData = await this.submitSummaryCallback(submitSummaryOptions);
            // Cumulatively add telemetry properties based on how far generateSummary went.
            const referenceSequenceNumber = summaryData.referenceSequenceNumber;
            summarizeTelemetryProps = {
                ...summarizeTelemetryProps,
                referenceSequenceNumber,
                minimumSequenceNumber: summaryData.minimumSequenceNumber,
                opsSinceLastAttempt: referenceSequenceNumber - lastAttemptRefSeqNum,
                opsSinceLastSummary: referenceSequenceNumber - this.heuristicData.lastSuccessfulSummary.refSequenceNumber,
                stage: summaryData.stage,
            };
            summarizeTelemetryProps = this.addSummaryDataToTelemetryProps(summaryData, summarizeTelemetryProps);
            if (summaryData.stage !== "submit") {
                const errorCode = "submitSummaryFailure";
                const retriableError = summaryData.error ?? new RetriableSummaryError((0, exports.getFailMessage)(errorCode));
                return fail(errorCode, retriableError, summarizeTelemetryProps, {
                    stage: summaryData.stage,
                });
            }
            /**
             * With incremental summaries, if the full tree was not summarized, only data stores that changed should
             * be summarized. A data store is considered changed if either or both of the following is true:
             * - It has received an op.
             * - Its reference state changed, i.e., it went from referenced to unreferenced or vice-versa.
             *
             * In the extreme case, every op can be for a different data store and each op can result in the reference
             * state change of multiple data stores. So, the total number of data stores that are summarized should not
             * exceed the number of ops since last summary + number of data store whose reference state changed.
             */
            if (!submitSummaryOptions.fullTree) {
                const { summarizedDataStoreCount, gcStateUpdatedDataStoreCount = 0 } = summaryData.summaryStats;
                if (summarizedDataStoreCount >
                    gcStateUpdatedDataStoreCount + this.heuristicData.opsSinceLastSummary) {
                    summaryLogger.sendErrorEvent({
                        eventName: "IncrementalSummaryViolation",
                        summarizedDataStoreCount,
                        gcStateUpdatedDataStoreCount,
                        opsSinceLastSummary: this.heuristicData.opsSinceLastSummary,
                    });
                }
            }
            // Log event here on summary success only, as Summarize_cancel duplicates failure logging.
            summarizeEvent.reportEvent("generate", { ...summarizeTelemetryProps });
            resultsBuilder.summarySubmitted.resolve({ success: true, data: summaryData });
        }
        catch (error) {
            return fail("submitSummaryFailure", (0, internal_4.wrapError)(error, (message) => new RetriableSummaryError(message, (0, internal_3.getRetryDelaySecondsFromError)(error))), undefined /* properties */, {
                stage: "unknown",
            });
        }
        finally {
            if (summaryData === undefined) {
                this.heuristicData.recordAttempt();
            }
            this.summarizeTimer.clear();
        }
        try {
            const pendingTimeoutP = this.pendingAckTimer.start();
            const summary = this.summaryWatcher.watchSummary(summaryData.clientSequenceNumber);
            // Wait for broadcast
            const waitBroadcastResult = await raceTimer(summary.waitBroadcast(), pendingTimeoutP, cancellationToken);
            if (waitBroadcastResult.result === "cancelled") {
                const errorCode = "disconnect";
                return fail(errorCode, new RetriableSummaryError((0, exports.getFailMessage)(errorCode)));
            }
            if (waitBroadcastResult.result !== "done") {
                // The summary op may not have been received within the timeout due to a transient error. So,
                // fail with a retriable error to re-attempt the summary if possible.
                const errorCode = "summaryOpWaitTimeout";
                return fail(errorCode, new RetriableSummaryError((0, exports.getFailMessage)(errorCode), 0 /* retryAfterSeconds */));
            }
            const summarizeOp = waitBroadcastResult.value;
            const broadcastDuration = Date.now() - this.heuristicData.lastAttempt.summaryTime;
            resultsBuilder.summaryOpBroadcasted.resolve({
                success: true,
                data: { summarizeOp, broadcastDuration },
            });
            this.heuristicData.lastAttempt.summarySequenceNumber = summarizeOp.sequenceNumber;
            summaryLogger.sendTelemetryEvent({
                eventName: "Summarize_Op",
                duration: broadcastDuration,
                referenceSequenceNumber: summarizeOp.referenceSequenceNumber,
                summarySequenceNumber: summarizeOp.sequenceNumber,
                handle: summarizeOp.contents.handle,
            });
            // Wait for ack/nack
            const waitAckNackResult = await raceTimer(summary.waitAckNack(), pendingTimeoutP, cancellationToken);
            if (waitAckNackResult.result === "cancelled") {
                const errorCode = "disconnect";
                return fail(errorCode, new RetriableSummaryError((0, exports.getFailMessage)(errorCode)));
            }
            if (waitAckNackResult.result !== "done") {
                const errorCode = "summaryAckWaitTimeout";
                // The summary ack may not have been received within the timeout due to a transient error. So,
                // fail with a retriable error to re-attempt the summary if possible.
                return fail(errorCode, new RetriableSummaryError((0, exports.getFailMessage)(errorCode), 0 /* retryAfterSeconds */));
            }
            const ackNackOp = waitAckNackResult.value;
            this.pendingAckTimer.clear();
            // Update for success/failure
            const ackNackDuration = Date.now() - this.heuristicData.lastAttempt.summaryTime;
            // adding new properties
            summarizeTelemetryProps = {
                ackWaitDuration: ackNackDuration,
                ackNackSequenceNumber: ackNackOp.sequenceNumber,
                summarySequenceNumber: ackNackOp.contents.summaryProposal.summarySequenceNumber,
                ...summarizeTelemetryProps,
            };
            if (ackNackOp.type === internal_2.MessageType.SummaryAck) {
                this.heuristicData.markLastAttemptAsSuccessful();
                this.successfulSummaryCallback();
                summarizeEvent.end({
                    ...summarizeTelemetryProps,
                    handle: ackNackOp.contents.handle,
                });
                // This processes the summary ack of the successful summary. This is so that the next summary does not
                // start before the ack of the previous summary is processed.
                await this.refreshLatestSummaryCallback({
                    proposalHandle: summarizeOp.contents.handle,
                    ackHandle: ackNackOp.contents.handle,
                    summaryRefSeq: summarizeOp.referenceSequenceNumber,
                    summaryLogger,
                });
                resultsBuilder.receivedSummaryAckOrNack.resolve({
                    success: true,
                    data: {
                        summaryAckOp: ackNackOp,
                        ackNackDuration,
                    },
                });
            }
            else {
                // Check for retryDelay in summaryNack response.
                (0, internal_1.assert)(ackNackOp.type === internal_2.MessageType.SummaryNack, 0x274 /* "type check" */);
                const summaryNack = ackNackOp.contents;
                const errorMessage = summaryNack?.message;
                const retryAfterSeconds = summaryNack?.retryAfter;
                const errorCode = "summaryNack";
                // pre-0.58 error message prefix: summaryNack
                const error = new RetriableSummaryError((0, exports.getFailMessage)(errorCode), retryAfterSeconds, {
                    errorMessage,
                });
                (0, internal_1.assert)((0, internal_3.getRetryDelaySecondsFromError)(error) === retryAfterSeconds, 0x25f /* "retryAfterSeconds" */);
                // This will only set resultsBuilder.receivedSummaryAckOrNack, as other promises are already set.
                return fail(errorCode, error, { ...summarizeTelemetryProps, nackRetryAfter: retryAfterSeconds }, undefined /* submitFailureResult */, { summaryNackOp: ackNackOp, ackNackDuration });
            }
        }
        finally {
            this.pendingAckTimer.clear();
        }
    }
    addSummaryDataToTelemetryProps(summaryData, initialProps) {
        switch (summaryData.stage) {
            case "base":
                return initialProps;
            case "generate":
                return {
                    ...initialProps,
                    ...summaryData.summaryStats,
                    generateDuration: summaryData.generateDuration,
                };
            case "upload":
                return {
                    ...initialProps,
                    ...summaryData.summaryStats,
                    generateDuration: summaryData.generateDuration,
                    handle: summaryData.handle,
                    uploadDuration: summaryData.uploadDuration,
                };
            case "submit":
                return {
                    ...initialProps,
                    ...summaryData.summaryStats,
                    generateDuration: summaryData.generateDuration,
                    handle: summaryData.handle,
                    uploadDuration: summaryData.uploadDuration,
                    clientSequenceNumber: summaryData.clientSequenceNumber,
                    hasMissingOpData: this.heuristicData.hasMissingOpData,
                    opsSizesSinceLastSummary: this.heuristicData.totalOpsSize,
                    nonRuntimeOpsSinceLastSummary: this.heuristicData.numNonRuntimeOps,
                    runtimeOpsSinceLastSummary: this.heuristicData.numRuntimeOps,
                };
            default:
                (0, internal_1.assert)(true, 0x397 /* Unexpected summary stage */);
        }
        return initialProps;
    }
    summarizeTimerHandler(time, count) {
        this.logger.sendPerformanceEvent({
            eventName: "SummarizeTimeout",
            timeoutTime: time,
            timeoutCount: count,
        });
        if (count < maxSummarizeTimeoutCount) {
            // Double and start a new timer
            const nextTime = time * 2;
            this.summarizeTimer.start(nextTime, () => this.summarizeTimerHandler(nextTime, count + 1));
        }
    }
    dispose() {
        this.summarizeTimer.clear();
    }
}
exports.SummaryGenerator = SummaryGenerator;
//# sourceMappingURL=summaryGenerator.js.map