/**
 * Validate that under significant WiredTiger cache pressure an operation can fail
 * with TemporarilyUnavailable error.
 *
 * @tags: [
 *   # Exclude in-memory engine, rollbacks due to pinned cache content rely on eviction.
 *   requires_persistence,
 *   requires_replication,
 *   requires_wiredtiger,
 * ]
 */
import {ReplSetTest} from "jstests/libs/replsettest.js";

const replSet = new ReplSetTest({
    nodes: 1,
    nodeOptions: {
        wiredTigerCacheSizeGB: 0.256,
        setParameter: {
            logComponentVerbosity: tojson({control: 1, write: 1}),
            enableTemporarilyUnavailableExceptions: true,
            // Lower these values from the defaults to speed up the test.
            temporarilyUnavailableMaxRetries: 3,
            temporarilyUnavailableBackoffBaseMs: 10,
        }
    },
});
replSet.startSet();
replSet.initiate();
const db = replSet.getPrimary().getDB("test");

// Generate a workload pinning enough dirty data in cache that causes WiredTiger
// to roll back transactions. This workload is adapted from the reproducer in the
// SERVER-61909 ticket description.
assert.commandWorked(db.c.createIndex({x: "text"}));
let doc = {x: []};
for (let j = 0; j < 50000; j++)
    doc.x.push("" + Math.random() + Math.random());

(function temporarilyUnavailableNonTransaction() {
    const serverStatusBefore = db.serverStatus();
    let caughtTUerror = false;
    let attempts;
    for (attempts = 1; attempts <= 20; attempts++) {
        print("temporarilyUnavailableNonTransaction attempt " + attempts);
        const ret = db.c.insert(doc);

        if (ret["nInserted"] === 1) {
            // The write succeeded.
            continue;
        }
        assert.eq(0, ret["nInserted"]);
        assert.commandFailedWithCode(ret, ErrorCodes.TemporarilyUnavailable);
        caughtTUerror = true;
        jsTestLog("returned the expected TemporarilyUnavailable code at attempt " + attempts);
        break;
    }

    assert(caughtTUerror,
           "did not return the expected TemporarilyUnavailable error after " + (attempts - 1) +
               " attempts");
    const serverStatusAfter = db.serverStatus();
    assert.gt(serverStatusAfter.metrics.operation.temporarilyUnavailableErrors,
              serverStatusBefore.metrics.operation.temporarilyUnavailableErrors);
    assert.gt(serverStatusAfter.metrics.operation.temporarilyUnavailableErrorsEscaped,
              serverStatusBefore.metrics.operation.temporarilyUnavailableErrorsEscaped);
})();

(function temporarilyUnavailableInTransactionIsConvertedToWriteConflict() {
    // Inside a transaction, TemporarilyUnavailable errors should be converted to
    // WriteConflicts and tagged as TransientTransactionErrors.
    const serverStatusBefore = db.serverStatus();
    let caughtWriteConflict = false;
    let attempts;
    let ret;
    for (attempts = 1; attempts <= 20; attempts++) {
        print("temporarilyUnavailableInTransactionIsConvertedToWriteConflict attempt " + attempts);
        const session = db.getMongo().startSession();
        session.startTransaction();
        const sessionDB = session.getDatabase("test");
        ret = sessionDB.c.insert(doc);

        if (ret["nInserted"] === 1) {
            // The write succeeded.
            session.commitTransaction();
            continue;
        }
        assert.commandFailedWithCode(ret, ErrorCodes.WriteConflict, ret);
        assert(ret.hasOwnProperty("errorLabels"), ret);
        assert.contains("TransientTransactionError", ret.errorLabels, ret);
        caughtWriteConflict = true;
        jsTestLog("returned the expected WriteConflict code at attempt " + attempts);
        session.abortTransaction();
        break;
    }

    assert(caughtWriteConflict,
           "did not return the expected WriteConflict error after " + (attempts - 1) +
               " attempts. Result: " + tojson(ret));

    const serverStatusAfter = db.serverStatus();
    assert.gt(
        serverStatusAfter.metrics.operation.temporarilyUnavailableErrorsConvertedToWriteConflict,
        serverStatusBefore.metrics.operation.temporarilyUnavailableErrorsConvertedToWriteConflict);
    assert.eq(serverStatusAfter.metrics.operation.temporarilyUnavailableErrors,
              serverStatusBefore.metrics.operation.temporarilyUnavailableErrors);
    assert.eq(serverStatusAfter.metrics.operation.temporarilyUnavailableErrorsEscaped,
              serverStatusBefore.metrics.operation.temporarilyUnavailableErrorsEscaped);
})();

replSet.stopSet();