// Tests multi-document transactions metrics in the serverStatus output.
// @tags: [uses_transactions]
(function() {
"use strict";
load("jstests/libs/fail_point_util.js");  // For configureFailPoint

// Verifies that the server status response has the fields that we expect.
function verifyServerStatusFields(serverStatusResponse) {
    assert(serverStatusResponse.hasOwnProperty("transactions"),
           "Expected the serverStatus response to have a 'transactions' field\n" +
               tojson(serverStatusResponse));
    assert(serverStatusResponse.transactions.hasOwnProperty("currentActive"),
           "The 'transactions' field in serverStatus did not have the 'currentActive' field\n" +
               tojson(serverStatusResponse.transactions));
    assert(serverStatusResponse.transactions.hasOwnProperty("currentInactive"),
           "The 'transactions' field in serverStatus did not have the 'currentInactive' field\n" +
               tojson(serverStatusResponse.transactions));
    assert(serverStatusResponse.transactions.hasOwnProperty("currentOpen"),
           "The 'transactions' field in serverStatus did not have the 'currentOpen' field\n" +
               tojson(serverStatusResponse.transactions));
    assert(serverStatusResponse.transactions.hasOwnProperty("totalAborted"),
           "The 'transactions' field in serverStatus did not have the 'totalAborted' field\n" +
               tojson(serverStatusResponse.transactions));
    assert(serverStatusResponse.transactions.hasOwnProperty("totalCommitted"),
           "The 'transactions' field in serverStatus did not have the 'totalCommitted' field\n" +
               tojson(serverStatusResponse.transactions));
    assert(serverStatusResponse.transactions.hasOwnProperty("totalStarted"),
           "The 'transactions' field in serverStatus did not have the 'totalStarted' field\n" +
               tojson(serverStatusResponse.transactions));
}

// Verifies that the given value of the server status response is incremented in the way
// we expect.
function verifyServerStatusChange(initialStats, newStats, valueName, expectedIncrement) {
    assert.eq(initialStats[valueName] + expectedIncrement,
              newStats[valueName],
              "expected " + valueName + " to increase by " + expectedIncrement);
}

// Set up the replica set.
const rst = new ReplSetTest({nodes: 1});
rst.startSet();
rst.initiate();
const primary = rst.getPrimary();

// Set up the test database.
const dbName = "test";
const collName = "server_transactions_metrics";
const testDB = primary.getDB(dbName);
const adminDB = rst.getPrimary().getDB('admin');
testDB.runCommand({drop: collName, writeConcern: {w: "majority"}});
assert.commandWorked(testDB.runCommand({create: collName, writeConcern: {w: "majority"}}));

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

// Get state of server status before the transaction.
let initialStatus = assert.commandWorked(testDB.adminCommand({serverStatus: 1}));
verifyServerStatusFields(initialStatus);

// This transaction will commit.
jsTest.log("Start a transaction and then commit it.");

// Compare server status after starting a transaction with the server status before.
session.startTransaction();
assert.commandWorked(sessionColl.insert({_id: "insert-1"}));

let newStatus = assert.commandWorked(testDB.adminCommand({serverStatus: 1}));
verifyServerStatusFields(newStatus);
// Verify that the open transaction counter is incremented while inside the transaction.
verifyServerStatusChange(initialStatus.transactions, newStatus.transactions, "currentOpen", 1);
// Verify that when not running an operation, the transaction is inactive.
verifyServerStatusChange(initialStatus.transactions, newStatus.transactions, "currentActive", 0);
verifyServerStatusChange(initialStatus.transactions, newStatus.transactions, "currentInactive", 1);

// Compare server status after the transaction commit with the server status before.
assert.commandWorked(session.commitTransaction_forTesting());
newStatus = assert.commandWorked(testDB.adminCommand({serverStatus: 1}));
verifyServerStatusFields(newStatus);
verifyServerStatusChange(initialStatus.transactions, newStatus.transactions, "totalStarted", 1);
verifyServerStatusChange(initialStatus.transactions, newStatus.transactions, "totalCommitted", 1);
// Verify that current open counter is decremented on commit.
verifyServerStatusChange(initialStatus.transactions, newStatus.transactions, "currentOpen", 0);
// Verify that both active and inactive are 0 on commit.
verifyServerStatusChange(initialStatus.transactions, newStatus.transactions, "currentActive", 0);
verifyServerStatusChange(initialStatus.transactions, newStatus.transactions, "currentInactive", 0);

// This transaction will abort.
jsTest.log("Start a transaction and then abort it.");

// Compare server status after starting a transaction with the server status before.
session.startTransaction();
assert.commandWorked(sessionColl.insert({_id: "insert-2"}));

newStatus = assert.commandWorked(testDB.adminCommand({serverStatus: 1}));
verifyServerStatusFields(newStatus);
// Verify that the open transaction counter is incremented while inside the transaction.
verifyServerStatusChange(initialStatus.transactions, newStatus.transactions, "currentOpen", 1);
// Verify that when not running an operation, the transaction is inactive.
verifyServerStatusChange(initialStatus.transactions, newStatus.transactions, "currentActive", 0);
verifyServerStatusChange(initialStatus.transactions, newStatus.transactions, "currentInactive", 1);

// Compare server status after the transaction abort with the server status before.
assert.commandWorked(session.abortTransaction_forTesting());
newStatus = assert.commandWorked(testDB.adminCommand({serverStatus: 1}));
verifyServerStatusFields(newStatus);
verifyServerStatusChange(initialStatus.transactions, newStatus.transactions, "totalStarted", 2);
verifyServerStatusChange(initialStatus.transactions, newStatus.transactions, "totalCommitted", 1);
verifyServerStatusChange(initialStatus.transactions, newStatus.transactions, "totalAborted", 1);
// Verify that current open counter is decremented on abort.
verifyServerStatusChange(initialStatus.transactions, newStatus.transactions, "currentOpen", 0);
// Verify that both active and inactive are 0 on abort.
verifyServerStatusChange(initialStatus.transactions, newStatus.transactions, "currentActive", 0);
verifyServerStatusChange(initialStatus.transactions, newStatus.transactions, "currentInactive", 0);

// This transaction will abort due to a duplicate key insert.
jsTest.log("Start a transaction that will abort on a duplicated key error.");

// Compare server status after starting a transaction with the server status before.
session.startTransaction();
// Inserting a new document will work fine, and the transaction starts.
assert.commandWorked(sessionColl.insert({_id: "insert-3"}));

newStatus = assert.commandWorked(testDB.adminCommand({serverStatus: 1}));
verifyServerStatusFields(newStatus);
// Verify that the open transaction counter is incremented while inside the transaction.
verifyServerStatusChange(initialStatus.transactions, newStatus.transactions, "currentOpen", 1);
// Verify that when not running an operation, the transaction is inactive.
verifyServerStatusChange(initialStatus.transactions, newStatus.transactions, "currentActive", 0);
verifyServerStatusChange(initialStatus.transactions, newStatus.transactions, "currentInactive", 1);

// Compare server status after the transaction abort with the server status before.
// The duplicated insert will fail, causing the transaction to abort.
assert.commandFailedWithCode(sessionColl.insert({_id: "insert-3"}), ErrorCodes.DuplicateKey);
// Ensure that the transaction was aborted on failure.
assert.commandFailedWithCode(session.commitTransaction_forTesting(), ErrorCodes.NoSuchTransaction);
newStatus = assert.commandWorked(testDB.adminCommand({serverStatus: 1}));
verifyServerStatusFields(newStatus);
verifyServerStatusChange(initialStatus.transactions, newStatus.transactions, "totalStarted", 3);
verifyServerStatusChange(initialStatus.transactions, newStatus.transactions, "totalCommitted", 1);
verifyServerStatusChange(initialStatus.transactions, newStatus.transactions, "totalAborted", 2);
// Verify that current open counter is decremented on abort caused by an error.
verifyServerStatusChange(initialStatus.transactions, newStatus.transactions, "currentOpen", 0);
// Verify that both active and inactive are 0 on abort.
verifyServerStatusChange(initialStatus.transactions, newStatus.transactions, "currentActive", 0);
verifyServerStatusChange(initialStatus.transactions, newStatus.transactions, "currentInactive", 0);

// Hang the transaction on a failpoint in the middle of an operation to check active and
// inactive counters while operation is running inside a transaction.
jsTest.log("Start a transaction that will hang in the middle of an operation due to a fail point.");
const fpHangDuringBatchUpdate = configureFailPoint(primary, 'hangDuringBatchUpdate');

const transactionFn = function() {
    const collName = 'server_transactions_metrics';
    const session = db.getMongo().startSession({causalConsistency: false});
    const sessionDb = session.getDatabase('test');
    const sessionColl = sessionDb[collName];

    session.startTransaction({readConcern: {level: 'snapshot'}});
    assert.commandWorked(sessionColl.update({}, {"update-1": 2}));
    assert.commandWorked(session.commitTransaction_forTesting());
};
const transactionProcess = startParallelShell(transactionFn, primary.port);

// Keep running currentOp() until we see the transaction subdocument.
assert.soon(function() {
    const transactionFilter = {
        active: true,
        'lsid': {$exists: true},
        'transaction.parameters.txnNumber': {$eq: 0}
    };
    return 1 === adminDB.aggregate([{$currentOp: {}}, {$match: transactionFilter}]).itcount();
});

jsTestLog("Wait until the operation is in the middle of executing.");
fpHangDuringBatchUpdate.wait();

newStatus = assert.commandWorked(testDB.adminCommand({serverStatus: 1}));
verifyServerStatusFields(newStatus);
// Verify that the open transaction counter is incremented while inside the transaction.
verifyServerStatusChange(initialStatus.transactions, newStatus.transactions, "currentOpen", 1);
// Verify that the metrics show that the transaction is active while inside the operation.
verifyServerStatusChange(initialStatus.transactions, newStatus.transactions, "currentActive", 1);
verifyServerStatusChange(initialStatus.transactions, newStatus.transactions, "currentInactive", 0);

// Now the transaction can proceed.
fpHangDuringBatchUpdate.off();
transactionProcess();
newStatus = assert.commandWorked(testDB.adminCommand({serverStatus: 1}));
verifyServerStatusFields(newStatus);
// Verify that current open counter is decremented on commit.
verifyServerStatusChange(initialStatus.transactions, newStatus.transactions, "currentOpen", 0);
// Verify that both active and inactive are 0 after the transaction finishes.
verifyServerStatusChange(initialStatus.transactions, newStatus.transactions, "currentActive", 0);
verifyServerStatusChange(initialStatus.transactions, newStatus.transactions, "currentInactive", 0);

// End the session and stop the replica set.
session.endSession();
rst.stopSet();
}());
