/**
 * Test the following behavior for a transaction on step down.
 *  1) Active transactional operations (like read and write) are killed and the transaction is
 * aborted, but the connection not closed.
 *  2) Inactive transaction is aborted.
 * @tags: [uses_transactions]
 */
(function() {
"use strict";

load("jstests/libs/curop_helpers.js");  // for waitForCurOpByFailPoint().

const testName = "txnsDuringStepDown";
const dbName = testName;
const collName = "testcoll";

const rst = new ReplSetTest({nodes: [{}, {rsConfig: {priority: 0}}]});
rst.startSet();
rst.initiate();

var primary = rst.getPrimary();
var db = primary.getDB(dbName);
var primaryAdmin = primary.getDB("admin");
var primaryColl = db[collName];
var collNss = primaryColl.getFullName();

jsTestLog("Writing data to collection.");
assert.commandWorked(primaryColl.insert({_id: 'readOp'}, {"writeConcern": {"w": 2}}));

TestData.dbName = dbName;
TestData.collName = collName;
TestData.skipRetryOnNetworkError = true;

function txnFunc() {
    jsTestLog("Starting a new transaction.");
    const session = db.getMongo().startSession();
    const sessionDb = session.getDatabase(TestData.dbName);
    const sessionColl = sessionDb[TestData.collName];
    session.startTransaction({writeConcern: {w: "majority"}});
    print(TestData.cmd);
    eval(TestData.cmd);

    // Validate that the connection is not closed on step down.
    assert.commandWorked(db.adminCommand({ping: 1}));
}

function runStepDown() {
    jsTestLog("Making primary step down.");
    assert.commandWorked(primaryAdmin.runCommand({"replSetStepDown": 30 * 60, "force": true}));

    // Wait until the primary transitioned to SECONDARY state.
    rst.waitForState(primary, ReplSetTest.State.SECONDARY);

    jsTestLog("Validating data.");
    assert.docEq([{_id: 'readOp'}], primaryColl.find().toArray());

    jsTestLog("Making old primary eligible to be re-elected.");
    assert.commandWorked(primaryAdmin.runCommand({replSetFreeze: 0}));
    rst.getPrimary();
}

function testTxnFailsWithCode({
    op,
    failPoint: failPoint = 'hangAfterPreallocateSnapshot',
    nss: nss = dbName + '.$cmd',
    preOp: preOp = ''
}) {
    jsTestLog("Enabling failPoint '" + failPoint + "' on primary.");
    assert.commandWorked(primary.adminCommand({
        configureFailPoint: failPoint,
        data: {shouldContinueOnInterrupt: true},
        mode: "alwaysOn"
    }));

    // Start transaction.
    TestData.cmd =
        preOp + `assert.commandFailedWithCode(${op}, ErrorCodes.InterruptedDueToReplStateChange);`;
    const waitForTxnShell = startParallelShell(txnFunc, primary.port);

    jsTestLog("Waiting for primary to reach failPoint '" + failPoint + "'.");
    waitForCurOpByFailPoint(primaryAdmin, nss, failPoint);

    // Call step down & validate data.
    runStepDown();

    // Wait for transaction shell to join.
    waitForTxnShell();

    // Disable fail point.
    assert.commandWorked(primaryAdmin.runCommand({configureFailPoint: failPoint, mode: 'off'}));
}

function testAbortOrCommitTxnFailsWithCode(params) {
    params["preOp"] = `sessionColl.insert({_id: 'abortOrCommitTxnOp'});`;
    params["nss"] = "admin.$cmd";
    testTxnFailsWithCode(params);
}

jsTestLog("Testing stepdown during read transaction.");
testTxnFailsWithCode({op: "sessionDb.runCommand({find: '" + collName + "', batchSize: 1})"});

jsTestLog("Testing stepdown during write transaction.");
testTxnFailsWithCode({op: "sessionColl.insert({_id: 'writeOp'})"});

jsTestLog("Testing stepdown during read-write transaction.");
testTxnFailsWithCode({
    op: "sessionDb.runCommand({findAndModify: '" + collName +
        "', query: {_id: 'readOp'}, remove: true})"
});

jsTestLog("Testing stepdown during commit transaction.");
testAbortOrCommitTxnFailsWithCode(
    {failPoint: "hangBeforeCommitingTxn", op: "session.commitTransaction_forTesting()"});

jsTestLog("Testing stepdown during running transaction in inactive state.");
// Do not start the transaction in parallel shell because when the parallel
// shell work is done, implicit call to "endSessions" and "abortTransaction"
// cmds are made. So, during step down we might not have any running
jsTestLog("Starting a new transaction.");
const session = db.getMongo().startSession();
const sessionDb = session.getDatabase(TestData.dbName);
const sessionColl = sessionDb[TestData.collName];
session.startTransaction({writeConcern: {w: "majority"}});
assert.commandWorked(sessionColl.insert({_id: 'inactiveTxnOp'}));

// Call step down & validate data.
runStepDown();

// Even though the transaction was aborted by the stepdown, we must still update the shell's
// transaction state to aborted.
assert.commandFailedWithCode(session.abortTransaction_forTesting(), ErrorCodes.NoSuchTransaction);

rst.stopSet();
})();
