/** * Tests that a DDL op (drop collection) begun after a prepared transaction fails during shutdown. * Sets up a prepared transaction, starts drop collection in a parallel shell, attempts to wait for * the drop collection operation to enqueue a lock request waiting behind the transaction, then * shuts down the server. On restarting, we expect to find the collection was not dropped, that * there is no way for a DDL op to slip through and succeed after transaction resources (locks) are * cleared away during shutdown. * * This is an attempt to test that DDL ops cannot succeed AFTER transaction state (including locks) * is cleared during shutdown. However, a lot goes on during shutdown and the DDL op probably gets * interrupted well before the transaction state is cleared: either during shutdown's repl * stepdown attempt or as soon as operations get interrupted for shutdown (which occurs before * transaction state is cleared). * * @tags: [requires_persistence, uses_prepare_transaction, uses_transactions] */ (function() { "use strict"; load("jstests/core/txns/libs/prepare_helpers.js"); load("jstests/libs/parallel_shell_helpers.js"); load('jstests/libs/test_background_ops.js'); const rst = new ReplSetTest({nodes: 1}); rst.startSet(); rst.initiate(); const dbName = "test"; const collName = "ddl_op_behind_prepared_transaction_fails_in_shutdown"; let primary = rst.getPrimary(); const testDB = primary.getDB(dbName); const testColl = testDB.getCollection(collName); const txnDoc = { _id: 100 }; jsTest.log("Creating a collection '" + collName + "' with data in it..."); assert.commandWorked(testDB.createCollection(collName, {writeConcern: {w: "majority"}})); let bulk = testColl.initializeUnorderedBulkOp(); for (let i = 0; i < 2; ++i) { bulk.insert({_id: i}); } assert.commandWorked(bulk.execute()); jsTest.log("Setting up a prepared transaction..."); const session = primary.startSession(); const sessionDB = session.getDatabase(dbName); const sessionColl = sessionDB.getCollection(collName); session.startTransaction(); assert.commandWorked(sessionColl.insert(txnDoc)); const prepareTimestamp = PrepareHelpers.prepareTransaction(session); function runDropCollection(dbName, collName) { jsTest.log("Dropping collection in parallel shell..."); // 'db' is defined in the parallel shell 'startParallelShell' will spin up. const res = db.getSiblingDB(dbName).runCommand({drop: collName}); assert.commandFailedWithCode( res, [ErrorCodes.InterruptedAtShutdown, ErrorCodes.InterruptedDueToReplStateChange], "parallel shell drop cmd completed in an unexpected way: " + tojson(res)); jsTest.log("Done dropping collection in parallel shell"); } // Use a failpoint to wait for the drop operation to get as close as possible to a lock request // before we release it and wait 1 second more for it to hopefully have time to enqueue a lock // request. It takes a while for the parallel shell to start up, establish a connection with the // server for the drop operation, etc., and we do not want to interrupt it earlier than lock // acquisition with the shutdown signal. // // This is best-effort, not deterministic, since we cannot place a fail point directly in the // locking code as that would hang everything rather than just drop. assert.commandWorked(primary.adminCommand( {configureFailPoint: 'hangDropCollectionBeforeLockAcquisition', mode: 'alwaysOn'})); let joinDropCollection; try { jsTest.log("Starting a parallel shell to concurrently run drop collection..."); joinDropCollection = startParallelShell(funWithArgs(runDropCollection, dbName, collName), primary.port); jsTest.log("Waiting for drop collection to block behind the prepared transaction..."); checkLog.contains(primary, "Hanging drop collection before lock acquisition while fail point is set"); } finally { assert.commandWorked(primary.adminCommand( {configureFailPoint: 'hangDropCollectionBeforeLockAcquisition', mode: 'off'})); } sleep(1 * 1000); jsTest.log("Restarting the mongod..."); // Skip validation because it requires a lock that the prepared transaction is blocking. rst.stop(primary, undefined, {skipValidation: true}); rst.start(primary, {}, true /*restart*/); primary = rst.getPrimary(); joinDropCollection(); const numDocs = primary.getDB(dbName).getCollection(collName).find().length(); // We expect two documents because the third is in an uncommitted transaction and not visible. assert.eq( 2, numDocs, "Expected '" + collName + "' to find 2 documents, found " + numDocs + ". Drop collection may have succeeded during shutdown while a transaction was in the " + "prepared state."); // We will check that the prepared transaction is still active as expected, since we are here. assert.commandFailedWithCode(primary.getDB(dbName).runCommand({ find: collName, filter: txnDoc, readConcern: {afterClusterTime: prepareTimestamp}, maxTimeMS: 5000 }), ErrorCodes.MaxTimeMSExpired); // Skip validation because it requires a lock that the prepared transaction is blocking. rst.stopSet(true /*use default exit signal*/, false /*forRestart*/, {skipValidation: true}); })();