/** * Tests that prepared transactions can survive state transitions from SECONDARY to RECOVERING and * RECOVERING to SECONDARY. * * @tags: [uses_transactions, uses_prepare_transaction] */ (function() { "use strict"; load("jstests/core/txns/libs/prepare_helpers.js"); const replSet = new ReplSetTest({nodes: 2}); replSet.startSet(); replSet.initiate(); const primary = replSet.getPrimary(); const secondary = replSet.getSecondary(); const dbName = "test"; const collName = "prepare_transaction_survives_state_transitions_to_and_from_recovering"; const testDB = primary.getDB(dbName); const testColl = testDB.getCollection(collName); assert.commandWorked(testDB.runCommand({create: collName})); assert.commandWorked(testColl.insert({_id: 1})); const session1 = primary.startSession({causalConsistency: false}); const sessionDB1 = session1.getDatabase(dbName); const sessionColl1 = sessionDB1.getCollection(collName); const session2 = primary.startSession({causalConsistency: false}); const sessionDB2 = session2.getDatabase(dbName); const sessionColl2 = sessionDB2.getCollection(collName); jsTestLog("Starting a transaction and putting it into prepare"); session1.startTransaction(); assert.commandWorked(sessionColl1.update({_id: 1}, {_id: 1, a: 1})); const prepareTimestamp1 = PrepareHelpers.prepareTransaction(session1); jsTestLog("Starting a second transaction and putting it into prepare"); session2.startTransaction(); assert.commandWorked(sessionColl2.insert({_id: 2})); const prepareTimestamp2 = PrepareHelpers.prepareTransaction(session2); replSet.awaitReplication(); jsTestLog("Putting secondary in maintenance mode so it will transition to RECOVERING"); assert.commandWorked(secondary.adminCommand({replSetMaintenance: 1})); replSet.waitForState(secondary, ReplSetTest.State.RECOVERING); jsTestLog("Commiting the second prepared transaction while a node is in the RECOVERING state"); assert.commandWorked(PrepareHelpers.commitTransaction(session2, prepareTimestamp2)); replSet.awaitReplication(); jsTestLog("Taking secondary out of maintenance mode so it will transition back to SECONDARY"); assert.commandWorked(secondary.adminCommand({replSetMaintenance: 0})); replSet.waitForState(secondary, ReplSetTest.State.SECONDARY); jsTestLog("Stepping up the secondary"); replSet.stepUp(secondary); replSet.waitForState(secondary, ReplSetTest.State.PRIMARY); const newPrimary = replSet.getPrimary(); const newPrimaryDB = newPrimary.getDB(dbName); // Create a proxy session to reuse the session state of the old primary. const newSession = new _DelegatingDriverSession(newPrimary, session1); jsTestLog("Make sure that the transaction is properly prepared"); // Make sure that we can't read changes to the document from the second transaction after // recovery. assert.eq(newPrimaryDB.getCollection(collName).find().toArray(), [{_id: 1}, {_id: 2}]); // Make sure that another write on the same document from the second transaction causes a write // conflict. assert.commandFailedWithCode( newPrimaryDB.runCommand( {update: collName, updates: [{q: {_id: 1}, u: {$set: {a: 1}}}], maxTimeMS: 5 * 1000}), ErrorCodes.MaxTimeMSExpired); // Make sure that we cannot add other operations to the second transaction since it is prepared. assert.commandFailedWithCode( newSession.getDatabase(dbName).getCollection(collName).insert({_id: 3}), ErrorCodes.PreparedTransactionInProgress); jsTestLog("Verify that the locks from the prepared transaction are still held"); assert.commandFailedWithCode(newPrimaryDB.runCommand({drop: collName, maxTimeMS: 5 * 1000}), ErrorCodes.MaxTimeMSExpired); jsTestLog("Committing transaction"); assert.commandWorked(PrepareHelpers.commitTransaction(newSession, prepareTimestamp1)); replSet.awaitReplication(); replSet.stopSet(); }());