// Test TransientTransactionErrors error label in transactions. // @tags: [uses_transactions] (function() { "use strict"; load("jstests/libs/write_concern_util.js"); load("jstests/libs/parallelTester.js"); // For ScopedThread. const dbName = "test"; const collName = "no_error_labels_outside_txn"; const rst = new ReplSetTest({name: collName, nodes: 2}); const config = rst.getReplSetConfig(); config.members[1].priority = 0; rst.startSet(); rst.initiate(config); const primary = rst.getPrimary(); const secondary = rst.getSecondary(); const testDB = primary.getDB(dbName); const adminDB = testDB.getSiblingDB("admin"); const testColl = testDB.getCollection(collName); const sessionOptions = {causalConsistency: false}; let session = primary.startSession(sessionOptions); let sessionDb = session.getDatabase(dbName); let sessionColl = sessionDb.getCollection(collName); let secondarySession = secondary.startSession(sessionOptions); let secondarySessionDb = secondarySession.getDatabase(dbName); assert.commandWorked(testDB.createCollection(collName, {writeConcern: {w: "majority"}})); jsTest.log("Insert inside a transaction on secondary should fail but return error labels"); let txnNumber = 0; let res = secondarySessionDb.runCommand({ insert: collName, documents: [{_id: "insert-1"}], readConcern: {level: "snapshot"}, txnNumber: NumberLong(txnNumber), startTransaction: true, autocommit: false }); assert.commandFailedWithCode(res, ErrorCodes.NotMaster); assert.eq(res.errorLabels, ["TransientTransactionError"]); jsTest.log("Insert outside a transaction on secondary should fail but not return error labels"); txnNumber++; // Insert as a retryable write. res = secondarySessionDb.runCommand( {insert: collName, documents: [{_id: "insert-1"}], txnNumber: NumberLong(txnNumber)}); assert.commandFailedWithCode(res, ErrorCodes.NotMaster); assert(!res.hasOwnProperty("errorLabels")); secondarySession.endSession(); jsTest.log("failCommand should be able to return errors with TransientTransactionError"); assert.commandWorked(testDB.adminCommand({ configureFailPoint: "failCommand", mode: "alwaysOn", data: {errorCode: ErrorCodes.WriteConflict, failCommands: ["insert"]} })); session.startTransaction(); jsTest.log("WriteCommandError should have error labels inside transactions."); res = sessionColl.insert({_id: "write-fail-point"}); assert.commandFailedWithCode(res, ErrorCodes.WriteConflict); assert(res instanceof WriteCommandError); assert.eq(res.errorLabels, ["TransientTransactionError"]); res = testColl.insert({_id: "write-fail-point-outside-txn"}); jsTest.log("WriteCommandError should not have error labels outside transactions."); // WriteConflict will not be returned outside transactions in real cases, but it's fine for // testing purpose. assert.commandFailedWithCode(res, ErrorCodes.WriteConflict); assert(res instanceof WriteCommandError); assert(!res.hasOwnProperty("errorLabels")); assert.commandWorked(testDB.adminCommand({configureFailPoint: "failCommand", mode: "off"})); session.abortTransaction(); jsTest.log("WriteConflict returned by commitTransaction command is TransientTransactionError"); session.startTransaction(); assert.commandWorked(sessionColl.insert({_id: "commitTransaction-fail-point"})); assert.commandWorked(testDB.adminCommand({ configureFailPoint: "failCommand", mode: "alwaysOn", data: {errorCode: ErrorCodes.WriteConflict, failCommands: ["commitTransaction"]} })); res = session.commitTransaction_forTesting(); assert.commandFailedWithCode(res, ErrorCodes.WriteConflict); assert.eq(res.errorLabels, ["TransientTransactionError"]); assert.commandWorked(testDB.adminCommand({configureFailPoint: "failCommand", mode: "off"})); jsTest.log("NotMaster returned by commitTransaction command is not TransientTransactionError"); session.startTransaction(); assert.commandWorked(sessionColl.insert({_id: "commitTransaction-fail-point"})); assert.commandWorked(testDB.adminCommand({ configureFailPoint: "failCommand", mode: "alwaysOn", data: {errorCode: ErrorCodes.NotMaster, failCommands: ["commitTransaction"]} })); res = session.commitTransaction_forTesting(); assert.commandFailedWithCode(res, ErrorCodes.NotMaster); assert(!res.hasOwnProperty("errorLabels")); assert.commandWorked(testDB.adminCommand({configureFailPoint: "failCommand", mode: "off"})); jsTest.log("ShutdownInProgress returned by write commands is TransientTransactionError"); session.startTransaction(); assert.commandWorked(testDB.adminCommand({ configureFailPoint: "failCommand", mode: "alwaysOn", data: {errorCode: ErrorCodes.ShutdownInProgress, failCommands: ["insert"]} })); res = sessionColl.insert({_id: "commitTransaction-fail-point"}); assert.commandFailedWithCode(res, ErrorCodes.ShutdownInProgress); assert(res instanceof WriteCommandError); assert.eq(res.errorLabels, ["TransientTransactionError"]); assert.commandWorked(testDB.adminCommand({configureFailPoint: "failCommand", mode: "off"})); session.abortTransaction(); jsTest.log( "ShutdownInProgress returned by commitTransaction command is not TransientTransactionError"); session.startTransaction(); assert.commandWorked(sessionColl.insert({_id: "commitTransaction-fail-point"})); assert.commandWorked(testDB.adminCommand({ configureFailPoint: "failCommand", mode: "alwaysOn", data: {errorCode: ErrorCodes.ShutdownInProgress, failCommands: ["commitTransaction"]} })); res = session.commitTransaction_forTesting(); assert.commandFailedWithCode(res, ErrorCodes.ShutdownInProgress); assert(!res.hasOwnProperty("errorLabels")); assert.commandWorked(testDB.adminCommand({configureFailPoint: "failCommand", mode: "off"})); jsTest.log("LockTimeout should be TransientTransactionError"); // Start a transaction to hold the DBLock in IX mode so that drop will be blocked. session.startTransaction(); assert.commandWorked(sessionColl.insert({_id: "lock-timeout-1"})); function dropCmdFunc(testData, primaryHost, dbName, collName) { // Pass the TestData into the new shell so that jsTest.authenticate() can use the correct // credentials in auth test suites. TestData = testData; const primary = new Mongo(primaryHost); return primary.getDB(dbName).runCommand({drop: collName, writeConcern: {w: "majority"}}); } const thread = new ScopedThread(dropCmdFunc, TestData, primary.host, dbName, collName); thread.start(); // Wait for the drop to have a pending MODE_X lock on the database. assert.soon( function() { return adminDB .aggregate([ {$currentOp: {}}, {$match: {"command.drop": collName, waitingForLock: true}} ]) .itcount() === 1; }, function() { return "Failed to find drop in currentOp output: " + tojson(adminDB.aggregate([{$currentOp: {}}]).toArray()); }); // Start another transaction in a new session, which cannot acquire the database lock in time. let sessionOther = primary.startSession(sessionOptions); sessionOther.startTransaction(); res = sessionOther.getDatabase(dbName).getCollection(collName).insert({_id: "lock-timeout-2"}); assert.commandFailedWithCode(res, ErrorCodes.LockTimeout); assert(res instanceof WriteCommandError); assert.eq(res.errorLabels, ["TransientTransactionError"]); sessionOther.abortTransaction(); session.abortTransaction(); thread.join(); assert.commandWorked(thread.returnData()); session.endSession(); rst.stopSet(); }());