// Tests readConcern level metrics in the serverStatus output.
// @tags: [
//   requires_majority_read_concern,
//   requires_persistence,
//   uses_transactions,
// ]
// Verifies that the server status response has the fields that we expect.
import {ReplSetTest} from "jstests/libs/replsettest.js";

function verifyServerStatusFields(serverStatusResponse) {
    assert(serverStatusResponse.hasOwnProperty("readConcernCounters"),
           "Missing 'readConcernCounters' from serverStatus\n" + tojson(serverStatusResponse));

    assert(serverStatusResponse.readConcernCounters.hasOwnProperty("nonTransactionOps"),
           "Missing 'nonTransactionOps' from 'readConcernCounters'\n" +
               tojson(serverStatusResponse.readConcernCounters));
    assert(serverStatusResponse.readConcernCounters.nonTransactionOps.hasOwnProperty("none"),
           "Missing 'none' from 'readConcernCounters.nonTransactionOps'\n" +
               tojson(serverStatusResponse.readConcernCounters));
    assert(serverStatusResponse.readConcernCounters.nonTransactionOps.hasOwnProperty("local"),
           "Missing 'local' from 'readConcernCounters.nonTransactionOps'\n" +
               tojson(serverStatusResponse.readConcernCounters));
    assert(serverStatusResponse.readConcernCounters.nonTransactionOps.hasOwnProperty("available"),
           "Missing 'available' from 'readConcernCounters.nonTransactionOps'\n" +
               tojson(serverStatusResponse.readConcernCounters));
    assert(serverStatusResponse.readConcernCounters.nonTransactionOps.hasOwnProperty("majority"),
           "Missing 'majority' from 'readConcernCounters.nonTransactionOps'\n" +
               tojson(serverStatusResponse.readConcernCounters));
    assert(serverStatusResponse.readConcernCounters.nonTransactionOps.hasOwnProperty("snapshot"),
           "Missing 'snapshot' from 'readConcernCounters.nonTransactionOps'\n" +
               tojson(serverStatusResponse.readConcernCounters));
    assert(serverStatusResponse.readConcernCounters.nonTransactionOps.snapshot.hasOwnProperty(
               "withClusterTime"),
           "Missing 'withClusterTime' from 'readConcernCounters.nonTransactionOps.snapshot'\n" +
               tojson(serverStatusResponse.readConcernCounters));
    assert(serverStatusResponse.readConcernCounters.nonTransactionOps.snapshot.hasOwnProperty(
               "withoutClusterTime"),
           "Missing 'withoutClusterTime' from 'readConcernCounters.nonTransactionOps.snapshot'\n" +
               tojson(serverStatusResponse.readConcernCounters));
    assert(
        serverStatusResponse.readConcernCounters.nonTransactionOps.hasOwnProperty("linearizable"),
        "Missing 'linearizable' from 'readConcernCounters.nonTransactionOps'\n" +
            tojson(serverStatusResponse.readConcernCounters));

    assert(serverStatusResponse.readConcernCounters.hasOwnProperty("transactionOps"),
           "Missing 'transactionOps' from 'readConcernCounters'\n" +
               tojson(serverStatusResponse.readConcernCounters));
    assert(serverStatusResponse.readConcernCounters.transactionOps.hasOwnProperty("none"),
           "Missing 'none' from 'readConcernCounters.transactionOps'\n" +
               tojson(serverStatusResponse.readConcernCounters));
    assert(serverStatusResponse.readConcernCounters.transactionOps.hasOwnProperty("local"),
           "Missing 'local' from 'readConcernCounters.transactionOps'\n" +
               tojson(serverStatusResponse.readConcernCounters));
    assert(serverStatusResponse.readConcernCounters.transactionOps.hasOwnProperty("majority"),
           "Missing 'majority' from 'readConcernCounters.transactionOps'\n" +
               tojson(serverStatusResponse.readConcernCounters));
    assert(serverStatusResponse.readConcernCounters.transactionOps.hasOwnProperty("snapshot"),
           "Missing 'snapshot' from 'readConcernCounters.transactionOps'\n" +
               tojson(serverStatusResponse.readConcernCounters));
    assert(serverStatusResponse.readConcernCounters.transactionOps.snapshot.hasOwnProperty(
               "withClusterTime"),
           "Missing 'withClusterTime' from 'readConcernCounters.transactionOps.snapshot'\n" +
               tojson(serverStatusResponse.readConcernCounters));
    assert(serverStatusResponse.readConcernCounters.transactionOps.snapshot.hasOwnProperty(
               "withoutClusterTime"),
           "Missing 'withoutClusterTime' from 'readConcernCounters.transactionOps.snapshot'\n" +
               tojson(serverStatusResponse.readConcernCounters));
}

// Verifies that the given value of the server status response is incremented in the way
// we expect.
function verifyServerStatusChange(initialStats,
                                  newStats,
                                  fields,
                                  expectedIncrement,
                                  {isTransaction = false, atClusterTime = false} = {}) {
    verifyServerStatusFields(newStats);
    initialStats = initialStats.readConcernCounters;
    newStats = newStats.readConcernCounters;
    if (!Array.isArray(fields)) {
        fields = [fields];
    }

    if (fields.length === 1 && fields[0] === "none") {
        // Implicit default RC was used.
        fields.push("noneInfo.implicitDefault.local");
    }

    fields.forEach(field => {
        let fieldPath;
        if (isTransaction) {
            fieldPath = "transactionOps";
        } else {
            fieldPath = "nonTransactionOps";
        }
        if (field === "snapshot") {
            fieldPath = fieldPath + ".snapshot";
            if (atClusterTime) {
                field = "withClusterTime";
            } else {
                field = "withoutClusterTime";
            }
        }

        fieldPath = fieldPath + "." + field;
        let pathComponents = fieldPath.split(".");
        let initialParent = initialStats;
        let newParent = newStats;
        for (let i = 0; i < pathComponents.length - 1; i++) {
            assert(initialParent.hasOwnProperty(pathComponents[i]),
                   "initialStats did not contain component " + i + " of path " + fieldPath +
                       ", initialStats: " + tojson(initialStats));
            initialParent = initialParent[pathComponents[i]];

            assert(newParent.hasOwnProperty(pathComponents[i]),
                   "newStats did not contain component " + i + " of path " + fieldPath +
                       ", newStats: " + tojson(newStats));
            newParent = newParent[pathComponents[i]];
        }

        // Test the expected increment of the changed element. The element may not exist in the
        // initial stats, in which case it is treated as 0.
        let lastPathComponent = pathComponents[pathComponents.length - 1];
        let initialValue = 0;
        if (initialParent.hasOwnProperty(lastPathComponent)) {
            initialValue = initialParent[lastPathComponent];
        }
        assert(newParent.hasOwnProperty(lastPathComponent),
               "newStats did not contain last component of path " + fieldPath +
                   ", newStats: " + tojson(newStats));
        assert.eq(initialValue + expectedIncrement,
                  newParent[lastPathComponent],
                  "expected " + fieldPath + " to increase by " + expectedIncrement +
                      ", initialStats: " + tojson(initialStats) +
                      ", newStats: " + tojson(newStats));

        // Update the value of the field to the new value so we can compare the rest of the fields
        // using assert.docEq.
        initialParent[lastPathComponent] = newParent[lastPathComponent];
    });
    assert.docEq(initialStats,
                 newStats,
                 "Expected docEq after updating " + tojson(fields) +
                     ", initialStats: " + tojson(initialStats) + ", newStats: " + tojson(newStats));
}

const rst = new ReplSetTest({nodes: 1});
rst.startSet();
rst.initiate();
const primary = rst.getPrimary();
const dbName = "test";
const collName = "server_read_concern_metrics";
const testDB = primary.getDB(dbName);
const testColl = testDB[collName];
testDB.runCommand({drop: collName});
assert.commandWorked(testDB.createCollection(collName, {writeConcern: {w: "majority"}}));
assert.commandWorked(testColl.insert({_id: 0}, {writeConcern: {w: 'majority'}}));

const sessionOptions = {
    causalConsistency: false
};
const session = testDB.getMongo().startSession(sessionOptions);
const sessionDb = session.getDatabase(dbName);
const sessionColl = sessionDb[collName];

// Run an initial transaction to get config.transactions state into memory.
session.startTransaction();
assert.eq(sessionColl.find().itcount(), 1);
assert.commandWorked(session.abortTransaction_forTesting());

function getServerStatus(conn) {
    // Don't return defaultRWConcern because it may trigger a refresh of the read write concern
    // defaults, which unexpectedly increases the opReadConcernCounters.
    return assert.commandWorked(conn.adminCommand({serverStatus: 1, defaultRWConcern: false}));
}

// Get initial serverStatus.
let serverStatus = getServerStatus(testDB);
verifyServerStatusFields(serverStatus);

// Run a command without a readConcern.
assert.eq(testColl.find().itcount(), 1);
let newStatus = getServerStatus(testDB);
verifyServerStatusChange(serverStatus, newStatus, "none", 1);
serverStatus = newStatus;

// Non-transaction reads.
for (let level of ["none", "local", "available", "snapshot", "majority", "linearizable"]) {
    jsTestLog("Testing non-transaction reads with readConcern " + level);
    let readConcern = {};
    if (level !== "none") {
        readConcern = {level: level};
    }

    assert.commandWorked(testDB.runCommand({find: collName, readConcern: readConcern}));
    newStatus = getServerStatus(testDB);
    verifyServerStatusChange(serverStatus, newStatus, level, 1);
    serverStatus = newStatus;

    assert.commandWorked(testDB.runCommand(
        {aggregate: collName, pipeline: [], cursor: {}, readConcern: readConcern}));
    newStatus = getServerStatus(testDB);
    verifyServerStatusChange(serverStatus, newStatus, level, 1);
    serverStatus = newStatus;

    assert.commandWorked(
        testDB.runCommand({distinct: collName, key: "_id", readConcern: readConcern}));
    newStatus = getServerStatus(testDB);
    verifyServerStatusChange(serverStatus, newStatus, level, 1);
    serverStatus = newStatus;

    if (level !== "snapshot") {
        assert.commandWorked(testDB.runCommand({count: collName, readConcern: readConcern}));
        newStatus = getServerStatus(testDB);
        verifyServerStatusChange(serverStatus, newStatus, level, 1);
        serverStatus = newStatus;
    }

    if (level in ["none", "local", "available"]) {
        // mapReduce only support local and available.
        assert.commandWorked(testDB.runCommand({
            mapReduce: collName,
            map: function() {
                emit(this.a, this.a);
            },
            reduce: function(key, vals) {
                return 1;
            },
            out: {inline: 1},
            readConcern: readConcern
        }));
        newStatus = getServerStatus(testDB);
        verifyServerStatusChange(serverStatus, newStatus, level, 1);
        serverStatus = newStatus;
    }

    // getMore does not count toward readConcern metrics. getMore inherits the readConcern of the
    // originating command.
    let res = assert.commandWorked(
        testDB.runCommand({find: collName, batchSize: 0, readConcern: readConcern}));
    serverStatus = getServerStatus(testDB);
    assert.commandWorked(testDB.runCommand({getMore: res.cursor.id, collection: collName}));
    newStatus = getServerStatus(testDB);
    verifyServerStatusChange(serverStatus, newStatus, level, 0);
}

// Test non-transaction snapshot with atClusterTime.
let insertTimestamp =
    assert.commandWorked(testDB.runCommand({insert: "atClusterTime", documents: [{_id: 0}]}))
        .operationTime;
jsTestLog("Testing non-transaction reads with atClusterTime");
serverStatus = getServerStatus(testDB);
assert.commandWorked(testDB.runCommand(
    {find: "atClusterTime", readConcern: {level: "snapshot", atClusterTime: insertTimestamp}}));
newStatus = getServerStatus(testDB);
verifyServerStatusChange(serverStatus, newStatus, "snapshot", 1, {atClusterTime: true});
serverStatus = newStatus;

// Transaction reads.
for (let level of ["none", "local", "snapshot", "majority"]) {
    jsTestLog("Testing transaction reads with readConcern " + level);
    if (level === "none") {
        session.startTransaction();
    } else {
        session.startTransaction({readConcern: {level: level}});
    }
    assert.eq(sessionColl.find().itcount(), 1);
    newStatus = getServerStatus(testDB);
    verifyServerStatusChange(serverStatus, newStatus, level, 1, {isTransaction: true});
    serverStatus = newStatus;

    // Run a second find in the same transaction. It will inherit the readConcern from the
    // transaction.
    assert.eq(sessionColl.find().itcount(), 1);
    newStatus = getServerStatus(testDB);
    verifyServerStatusChange(serverStatus, newStatus, level, 1, {isTransaction: true});
    serverStatus = newStatus;

    // Run an insert in the same transaction. This should not count toward the readConcern metrics.
    assert.commandWorked(
        sessionDb.runCommand({insert: "transaction", documents: [{level: level}]}));
    newStatus = getServerStatus(testDB);
    verifyServerStatusChange(serverStatus, newStatus, level, 0, {isTransaction: true});
    assert.commandWorked(session.abortTransaction_forTesting());
    serverStatus = newStatus;
}

// Test transaction snapshot with atClusterTime.
insertTimestamp =
    assert.commandWorked(testDB.runCommand({insert: "atClusterTime", documents: [{_id: 1}]}))
        .operationTime;
jsTestLog("Testing transaction reads with atClusterTime");
session.startTransaction({readConcern: {level: "snapshot", atClusterTime: insertTimestamp}});
serverStatus = getServerStatus(testDB);

assert.eq(sessionColl.find().itcount(), 1);
newStatus = getServerStatus(testDB);
verifyServerStatusChange(
    serverStatus, newStatus, "snapshot", 1, {isTransaction: true, atClusterTime: true});
serverStatus = newStatus;

// Perform another read in the same transaction.
assert.eq(sessionColl.find().itcount(), 1);
newStatus = getServerStatus(testDB);
verifyServerStatusChange(
    serverStatus, newStatus, "snapshot", 1, {isTransaction: true, atClusterTime: true});
assert.commandWorked(session.abortTransaction_forTesting());

// Non-transaction CWRC reads.
for (let level of ["local", "available", "majority"]) {
    jsTestLog("Testing non-transaction reads with CWRC readConcern " + level);
    assert.commandWorked(
        primary.adminCommand({setDefaultRWConcern: 1, defaultReadConcern: {level: level}}));

    assert.commandWorked(testDB.runCommand({find: collName}));
    newStatus = getServerStatus(testDB);
    verifyServerStatusChange(serverStatus, newStatus, ["none", "noneInfo.CWRC." + level], 1);
    serverStatus = newStatus;

    assert.commandWorked(testDB.runCommand({aggregate: collName, pipeline: [], cursor: {}}));
    newStatus = getServerStatus(testDB);
    verifyServerStatusChange(serverStatus, newStatus, ["none", "noneInfo.CWRC." + level], 1);
    serverStatus = newStatus;

    assert.commandWorked(testDB.runCommand({distinct: collName, key: "_id"}));
    newStatus = getServerStatus(testDB);
    verifyServerStatusChange(serverStatus, newStatus, ["none", "noneInfo.CWRC." + level], 1);
    serverStatus = newStatus;

    assert.commandWorked(testDB.runCommand({count: collName}));
    newStatus = getServerStatus(testDB);
    verifyServerStatusChange(serverStatus, newStatus, ["none", "noneInfo.CWRC." + level], 1);
    serverStatus = newStatus;

    if (level in ["local", "available"]) {
        // mapReduce only support local and available.
        assert.commandWorked(testDB.runCommand({
            mapReduce: collName,
            map: function() {
                emit(this.a, this.a);
            },
            reduce: function(key, vals) {
                return 1;
            },
            out: {inline: 1}
        }));
        newStatus = getServerStatus(testDB);
        verifyServerStatusChange(serverStatus, newStatus, ["none", "noneInfo.CWRC." + level], 1);
        serverStatus = newStatus;
    }

    // getMore does not count toward readConcern metrics. getMore inherits the readConcern of the
    // originating command.
    let res = assert.commandWorked(testDB.runCommand({find: collName, batchSize: 0}));
    serverStatus = getServerStatus(testDB);
    assert.commandWorked(testDB.runCommand({getMore: res.cursor.id, collection: collName}));
    newStatus = getServerStatus(testDB);
    verifyServerStatusChange(serverStatus, newStatus, ["none", "noneInfo.CWRC." + level], 0);
}

// Transaction CWRC reads.
for (let level of ["local", "majority"]) {
    jsTestLog("Testing transaction reads with CWRC readConcern " + level);
    assert.commandWorked(
        primary.adminCommand({setDefaultRWConcern: 1, defaultReadConcern: {level: level}}));

    session.startTransaction({readConcern: {}});

    assert.eq(sessionColl.find().itcount(), 1);
    newStatus = getServerStatus(testDB);
    verifyServerStatusChange(
        serverStatus, newStatus, ["none", "noneInfo.CWRC." + level], 1, {isTransaction: true});
    serverStatus = newStatus;

    // Run a second find in the same transaction. It will inherit the readConcern from the
    // transaction.
    assert.eq(sessionColl.find().itcount(), 1);
    newStatus = getServerStatus(testDB);
    verifyServerStatusChange(
        serverStatus, newStatus, ["none", "noneInfo.CWRC." + level], 1, {isTransaction: true});
    serverStatus = newStatus;

    // Run an insert in the same transaction. This should not count toward the readConcern metrics.
    assert.commandWorked(
        sessionDb.runCommand({insert: "transaction", documents: [{level: level}]}));
    newStatus = getServerStatus(testDB);
    verifyServerStatusChange(
        serverStatus, newStatus, ["none", "noneInfo.CWRC." + level], 0, {isTransaction: true});
    assert.commandWorked(session.abortTransaction_forTesting());
    serverStatus = newStatus;
}

session.endSession();
rst.stopSet();