diff options
Diffstat (limited to 'jstests/sharding/internal_txns')
9 files changed, 1018 insertions, 491 deletions
diff --git a/jstests/sharding/internal_txns/libs/chunk_migration_test.js b/jstests/sharding/internal_txns/libs/chunk_migration_test.js index 02c490255e8..cc78170153e 100644 --- a/jstests/sharding/internal_txns/libs/chunk_migration_test.js +++ b/jstests/sharding/internal_txns/libs/chunk_migration_test.js @@ -14,6 +14,7 @@ 'use strict'; load('jstests/libs/chunk_manipulation_util.js'); +load('jstests/sharding/internal_txns/libs/fixture_helpers.js'); load('jstests/sharding/libs/sharded_transactions_helpers.js'); function InternalTransactionChunkMigrationTest(storeFindAndModifyImagesInSideCollection = true) { @@ -359,23 +360,25 @@ function InternalTransactionChunkMigrationTest(storeFindAndModifyImagesInSideCol testCase.setUpFunc(); const lsid = getTransactionSessionId(txnType, testCase); - const txnNumber = getNextTxnNumber(txnType, testCase); - - for (let i = 0; i < testCase.commands.length; i++) { - const command = testCase.commands[i]; - const cmdObj = Object.assign({}, command.cmdObj, {lsid, txnNumber, autocommit: false}); - if (i == 0) { - cmdObj.startTransaction = true; + runTxnRetryOnLockTimeoutError(() => { + const txnNumber = getNextTxnNumber(txnType, testCase); + + for (let i = 0; i < testCase.commands.length; i++) { + const command = testCase.commands[i]; + const cmdObj = + Object.assign({}, command.cmdObj, {lsid, txnNumber, autocommit: false}); + if (i == 0) { + cmdObj.startTransaction = true; + } + const res = assert.commandWorked(st.s.getDB(testCase.dbName).runCommand(cmdObj)); + command.checkResponseFunc(res); } - const res = assert.commandWorked(st.s.getDB(testCase.dbName).runCommand(cmdObj)); - command.checkResponseFunc(res); - } - - if (testCase.abortOnInitialTry) { - abortTransaction(lsid, txnNumber, testCase.isPreparedTxn); - } else { - commitTransaction(lsid, txnNumber); - } + if (testCase.abortOnInitialTry) { + abortTransaction(lsid, txnNumber, testCase.isPreparedTxn); + } else { + commitTransaction(lsid, txnNumber); + } + }); testCase.checkDocsFunc(!testCase.abortOnInitialTry /* isTxnCommitted */); } @@ -398,25 +401,28 @@ function InternalTransactionChunkMigrationTest(storeFindAndModifyImagesInSideCol const lsid = getTransactionSessionId(txnType, testCase); // Give the session a different txnUUID to simulate a retry from a different mongos. lsid.txnUUID = UUID(); - const txnNumber = getNextTxnNumber(txnType, testCase); + runTxnRetryOnLockTimeoutError(() => { + const txnNumber = getNextTxnNumber(txnType, testCase); - for (let i = 0; i < testCase.commands.length; i++) { - const command = testCase.commands[i]; + for (let i = 0; i < testCase.commands.length; i++) { + const command = testCase.commands[i]; - if (!isRetryAfterAbort && command.cmdObj.stmtId == -1) { - // The transaction has already committed and the statement in this command - // is not retryable so do not retry it. - continue; - } + if (!isRetryAfterAbort && command.cmdObj.stmtId == -1) { + // The transaction has already committed and the statement in this command + // is not retryable so do not retry it. + continue; + } - const cmdObj = Object.assign({}, command.cmdObj, {lsid, txnNumber, autocommit: false}); - if (i == 0) { - cmdObj.startTransaction = true; + const cmdObj = + Object.assign({}, command.cmdObj, {lsid, txnNumber, autocommit: false}); + if (i == 0) { + cmdObj.startTransaction = true; + } + const res = assert.commandWorked(st.s.getDB(testCase.dbName).runCommand(cmdObj)); + command.checkResponseFunc(res); } - const res = assert.commandWorked(st.s.getDB(testCase.dbName).runCommand(cmdObj)); - command.checkResponseFunc(res); - } - commitTransaction(lsid, txnNumber); + commitTransaction(lsid, txnNumber); + }); testCase.checkDocsFunc(true /* isTxnCommitted */); } diff --git a/jstests/sharding/internal_txns/libs/fixture_helpers.js b/jstests/sharding/internal_txns/libs/fixture_helpers.js new file mode 100644 index 00000000000..5e418726fc6 --- /dev/null +++ b/jstests/sharding/internal_txns/libs/fixture_helpers.js @@ -0,0 +1,37 @@ +function runTxnRetryOnTransientError(txnFunc) { + assert.soon(() => { + try { + txnFunc(); + return true; + } catch (e) { + if (e.hasOwnProperty('errorLabels') && + e.errorLabels.includes('TransientTransactionError') && + e.code != ErrorCodes.NoSuchTransaction) { + // Don't retry on a NoSuchTransaction error since it implies the transaction was + // aborted so we should propagate the error instead. + jsTest.log("Failed to run transaction due to a transient error " + tojson(e)); + return false; + } else { + throw e; + } + } + }); +} + +function runTxnRetryOnLockTimeoutError(txnFunc) { + assert.soon(() => { + try { + txnFunc(); + return true; + } catch (e) { + if (e.hasOwnProperty('errorLabels') && + e.errorLabels.includes('TransientTransactionError') && + e.code == ErrorCodes.LockTimeout) { + jsTest.log("Failed to run transaction due to a transient error " + tojson(e)); + return false; + } else { + throw e; + } + } + }); +} diff --git a/jstests/sharding/internal_txns/libs/resharding_test.js b/jstests/sharding/internal_txns/libs/resharding_test.js index c5ded8792b1..ce4d96b9858 100644 --- a/jstests/sharding/internal_txns/libs/resharding_test.js +++ b/jstests/sharding/internal_txns/libs/resharding_test.js @@ -14,6 +14,7 @@ 'use strict'; load("jstests/libs/discover_topology.js"); +load('jstests/sharding/internal_txns/libs/fixture_helpers.js'); load("jstests/sharding/libs/resharding_test_fixture.js"); load('jstests/sharding/libs/sharded_transactions_helpers.js'); @@ -324,38 +325,25 @@ function InternalTransactionReshardingTest( testCase.setUpFunc(); const lsid = getTransactionSessionId(txnType, testCase); - - while (true) { + runTxnRetryOnTransientError(() => { const txnNumber = getNextTxnNumber(txnType, testCase); - try { - for (let i = 0; i < testCase.commands.length; i++) { - const command = testCase.commands[i]; - const cmdObj = - Object.assign({}, command.cmdObj, {lsid, txnNumber, autocommit: false}); - if (i == 0) { - cmdObj.startTransaction = true; - } - const res = assert.commandWorked(mongosConn.getDB(kDbName).runCommand(cmdObj)); - command.checkResponseFunc(res); - } - - if (testCase.abortOnInitialTry) { - abortTransaction(lsid, txnNumber, testCase.isPreparedTxn); - } else { - commitTransaction(lsid, txnNumber); - } - break; - } catch (e) { - if (e.hasOwnProperty('errorLabels') && - e.errorLabels.includes('TransientTransactionError') && - e.code != ErrorCodes.NoSuchTransaction) { - jsTest.log("Failed to run transaction due to a transient error " + tojson(e)); - } else { - throw e; + for (let i = 0; i < testCase.commands.length; i++) { + const command = testCase.commands[i]; + const cmdObj = + Object.assign({}, command.cmdObj, {lsid, txnNumber, autocommit: false}); + if (i == 0) { + cmdObj.startTransaction = true; } + const res = assert.commandWorked(mongosConn.getDB(kDbName).runCommand(cmdObj)); + command.checkResponseFunc(res); } - } + if (testCase.abortOnInitialTry) { + abortTransaction(lsid, txnNumber, testCase.isPreparedTxn); + } else { + commitTransaction(lsid, txnNumber); + } + }); testCase.checkDocsFunc(!testCase.abortOnInitialTry /* isTxnCommitted */); } @@ -381,50 +369,36 @@ function InternalTransactionReshardingTest( const lsid = getTransactionSessionId(txnType, testCase); // Give the session a different txnUUID to simulate a retry from a different mongos. lsid.txnUUID = UUID(); - - while (true) { + runTxnRetryOnTransientError(() => { const txnNumber = getNextTxnNumber(txnType, testCase); - try { - for (let i = 0; i < testCase.commands.length; i++) { - const command = testCase.commands[i]; + for (let i = 0; i < testCase.commands.length; i++) { + const command = testCase.commands[i]; - if (!isRetryAfterAbort && command.cmdObj.stmtId == -1) { - // The transaction has already committed and the statement in this command - // is not retryable so do not retry it. - continue; - } + if (!isRetryAfterAbort && command.cmdObj.stmtId == -1) { + // The transaction has already committed and the statement in this command + // is not retryable so do not retry it. + continue; + } - const cmdObj = - Object.assign({}, command.cmdObj, {lsid, txnNumber, autocommit: false}); - if (i == 0) { - cmdObj.startTransaction = true; - } - const res = mongosConn.getDB(kDbName).runCommand(cmdObj); - - if (expectRetryToSucceed) { - assert.commandWorked(res); - command.checkResponseFunc(res); - } else { - assert.commandFailedWithCode(res, ErrorCodes.IncompleteTransactionHistory); - return; - } + const cmdObj = + Object.assign({}, command.cmdObj, {lsid, txnNumber, autocommit: false}); + if (i == 0) { + cmdObj.startTransaction = true; } + const res = mongosConn.getDB(kDbName).runCommand(cmdObj); - commitTransaction(lsid, txnNumber); - break; - } catch (e) { - if (e.hasOwnProperty('errorLabels') && - e.errorLabels.includes('TransientTransactionError') && - e.code != ErrorCodes.NoSuchTransaction) { - jsTest.log("Failed to run transaction due to a transient error " + tojson(e)); + if (expectRetryToSucceed) { + assert.commandWorked(res); + command.checkResponseFunc(res); } else { - throw e; + assert.commandFailedWithCode(res, ErrorCodes.IncompleteTransactionHistory); + return; } } - } - - testCase.checkDocsFunc(true /* isTxnCommitted */); + commitTransaction(lsid, txnNumber); + testCase.checkDocsFunc(true /* isTxnCommitted */); + }); } /* diff --git a/jstests/sharding/internal_txns/libs/retryable_internal_transaction_test.js b/jstests/sharding/internal_txns/libs/retryable_internal_transaction_test.js index 6836540c441..0332843f72c 100644 --- a/jstests/sharding/internal_txns/libs/retryable_internal_transaction_test.js +++ b/jstests/sharding/internal_txns/libs/retryable_internal_transaction_test.js @@ -3,6 +3,7 @@ */ 'use strict'; +load('jstests/sharding/internal_txns/libs/fixture_helpers.js'); load('jstests/sharding/libs/sharded_transactions_helpers.js'); function getOplogEntriesForTxnWithRetries(rs, lsid, txnNumber) { @@ -58,6 +59,12 @@ function RetryableInternalTransactionTest(collectionOptions = {}) { return {id: UUID(), txnNumber: NumberLong(0), txnUUID: UUID()}; } + function setTxnFields(cmdObj, lsid, txnNumber) { + cmdObj.lsid = lsid; + cmdObj.txnNumber = NumberLong(txnNumber); + cmdObj.autocommit = false; + } + const getRandomOplogEntryLocation = function() { const locations = Object.values(kOplogEntryLocation); return locations[Math.floor(Math.random() * locations.length)]; @@ -106,60 +113,123 @@ function RetryableInternalTransactionTest(collectionOptions = {}) { assert.commandWorked(mongosTestDB.adminCommand(commitCmdObj)); } - function testRetryBasic(cmdObj, lsid, txnNumber, { - expectRetryToSucceed, + function testNonRetryableBasic(cmdObj, { + txnOptions, + testMode, expectFindAndModifyImageInOplog, - expectFindAndModifyImageInSideCollection, + expectFindAndModifyImageInSideCollection + }) { + // A findAndModify write statement in a non-retryable transaction will not generate a + // pre/post image. + assert(!expectFindAndModifyImageInOplog); + assert(!expectFindAndModifyImageInSideCollection); + jsTest.log("Testing retrying a non-retryable internal transaction"); + cmdObj.startTransaction = true; + + // Initial try. + const initialLsid = txnOptions.makeSessionIdFunc(); + let initialTxnNumber = 0; + runTxnRetryOnLockTimeoutError(() => { + initialTxnNumber++; + setTxnFields(cmdObj, initialLsid, initialTxnNumber); + assert.commandWorked(mongosTestDB.runCommand(cmdObj)); + commitTransaction(initialLsid, initialTxnNumber, txnOptions.isPreparedTxn); + }); + + const initialTxnStateBefore = getTransactionState(initialLsid, initialTxnNumber); + assert.eq(initialTxnStateBefore.oplogEntries.length, + (txnOptions.isPreparedTxn ? 2 : 1) + (expectFindAndModifyImageInOplog ? 1 : 0), + initialTxnStateBefore.oplogEntries); + assert.eq(initialTxnStateBefore.imageEntries.length, + expectFindAndModifyImageInSideCollection ? 1 : 0, + initialTxnStateBefore.imageEntries); + assertConsistentImageEntries(initialLsid, initialTxnNumber); + + setUpTestMode(testMode); + + // Retry. + assert.commandFailedWithCode(mongosTestDB.runCommand(cmdObj), + ErrorCodes.ConflictingOperationInProgress); + + const initialTxnStateAfter = getTransactionState(initialLsid, initialTxnNumber); + assert.eq(initialTxnStateBefore.oplogEntries, initialTxnStateAfter.oplogEntries); + assert.eq(initialTxnStateBefore.txnEntries, initialTxnStateAfter.txnEntries); + assert.eq(initialTxnStateBefore.imageEntries, initialTxnStateAfter.imageEntries); + + assert.commandWorked(mongosTestColl.remove({})); + } + + function testRetryableBasic(cmdObj, { txnOptions, testMode, - checkFunc + expectFindAndModifyImageInOplog, + expectFindAndModifyImageInSideCollection, + checkRetryResponseFunc }) { assert(!expectFindAndModifyImageInOplog || !expectFindAndModifyImageInSideCollection); + jsTest.log( + "Testing retrying a retryable internal transaction with one applyOps oplog entry"); + cmdObj.startTransaction = true; - const cmdObjToRetry = Object.assign(cmdObj, { - lsid: lsid, - txnNumber: NumberLong(txnNumber), - startTransaction: true, - autocommit: false, + // Initial try. + const initialLsid = txnOptions.makeSessionIdFunc(); + let initialTxnNumber = 0; + let initialRes; + runTxnRetryOnLockTimeoutError(() => { + initialTxnNumber++; + setTxnFields(cmdObj, initialLsid, initialTxnNumber); + initialRes = assert.commandWorked(mongosTestDB.runCommand(cmdObj)); + commitTransaction(initialLsid, initialTxnNumber, txnOptions.isPreparedTxn); }); - const initialRes = assert.commandWorked(mongosTestDB.runCommand(cmdObjToRetry)); - commitTransaction(lsid, txnNumber, txnOptions.isPreparedTxn); - - const txnStateBeforeRetry = getTransactionState(lsid, txnNumber); - assert.eq(txnStateBeforeRetry.oplogEntries.length, + const initialTxnStateBefore = getTransactionState(initialLsid, initialTxnNumber); + assert.eq(initialTxnStateBefore.oplogEntries.length, (txnOptions.isPreparedTxn ? 2 : 1) + (expectFindAndModifyImageInOplog ? 1 : 0), - txnStateBeforeRetry.oplogEntries); - assert.eq(txnStateBeforeRetry.imageEntries.length, + initialTxnStateBefore.oplogEntries); + assert.eq(initialTxnStateBefore.imageEntries.length, expectFindAndModifyImageInSideCollection ? 1 : 0, - txnStateBeforeRetry.imageEntries); - assertConsistentImageEntries(lsid, txnNumber); + initialTxnStateBefore.imageEntries); + assertConsistentImageEntries(initialLsid, initialTxnNumber); setUpTestMode(testMode); - const retryRes = mongosTestDB.runCommand(cmdObjToRetry); - if (expectRetryToSucceed) { - assert.commandWorked(retryRes); - checkFunc(initialRes, retryRes); - commitTransaction(lsid, txnNumber, txnOptions.isPreparedTxn, true /* isRetry */); - } else { - assert.commandFailedWithCode(retryRes, ErrorCodes.ConflictingOperationInProgress); - } + // Retry in the initial internal transaction. No need to commit since the transaction has + // already committed. + const retryRes = assert.commandWorked(mongosTestDB.runCommand(cmdObj)); + checkRetryResponseFunc(initialRes, retryRes); + + // Retry in a different internal transaction (running in an internal session with a + // different txnUUID) to simulate a retry from a different mongos. + const retryLsid = Object.assign({}, initialLsid, {txnUUID: UUID()}); + let retryTxnNumber = 0; + runTxnRetryOnLockTimeoutError(() => { + retryTxnNumber++; + setTxnFields(cmdObj, retryLsid, retryTxnNumber); + const retryRes = assert.commandWorked(mongosTestDB.runCommand(cmdObj)); + checkRetryResponseFunc(initialRes, retryRes); + commitTransaction( + retryLsid, retryTxnNumber, txnOptions.isPreparedTxn, true /* isRetry */); + }); - const txnStateAfterRetry = getTransactionState(lsid, txnNumber); - assert.eq(txnStateBeforeRetry.oplogEntries, txnStateAfterRetry.oplogEntries); - assert.eq(txnStateBeforeRetry.txnEntries, txnStateAfterRetry.txnEntries); - assert.eq(txnStateBeforeRetry.imageEntries, txnStateAfterRetry.imageEntries); + const initialTxnStateAfter = getTransactionState(initialLsid, initialTxnNumber); + assert.eq(initialTxnStateBefore.oplogEntries, initialTxnStateAfter.oplogEntries); + assert.eq(initialTxnStateBefore.txnEntries, initialTxnStateAfter.txnEntries); + assert.eq(initialTxnStateBefore.imageEntries, initialTxnStateAfter.imageEntries); + // The retry should not generate any persisted transaction state. + const retryTxnState = getTransactionState(retryLsid, retryTxnNumber); + assert.eq(retryTxnState.oplogEntries.length, 0, retryTxnState); + assert.eq(retryTxnState.txnEntries.length, 0, retryTxnState); + assert.eq(retryTxnState.imageEntries.length, 0, retryTxnState); assert.commandWorked(mongosTestColl.remove({})); } - function testRetryLargeTxn(cmdObj, lsid, txnNumber, { - expectFindAndModifyImageInOplog, - expectFindAndModifyImageInSideCollection, + function testRetryableLargeTxn(cmdObj, { txnOptions, testMode, - checkFunc + expectFindAndModifyImageInOplog, + expectFindAndModifyImageInSideCollection, + checkRetryResponseFunc }) { assert(!expectFindAndModifyImageInOplog || !expectFindAndModifyImageInSideCollection); @@ -171,18 +241,12 @@ function RetryableInternalTransactionTest(collectionOptions = {}) { return { insert: kCollName, documents: [Object.assign(doc, {y: new Array(kSize10MB).join("a")})], - lsid: lsid, - txnNumber: NumberLong(txnNumber), stmtId: NumberInt(stmtId++), - autocommit: false }; }; let makeCmdObjToRetry = (cmdObj) => { const cmdObjToRetry = Object.assign(cmdObj, { - lsid: lsid, - txnNumber: NumberLong(txnNumber), stmtId: NumberInt(stmtId), - autocommit: false, }); if (cmdObjToRetry.documents) { stmtId += cmdObjToRetry.documents.length; @@ -198,94 +262,131 @@ function RetryableInternalTransactionTest(collectionOptions = {}) { const insertCmdObj0 = Object.assign(makeInsertCmdObj({_id: -100, x: 100}), {startTransaction: true}); - const cmdObjToRetry = makeCmdObjToRetry(cmdObj); const insertCmdObj1 = makeInsertCmdObj({_id: -200, x: -200}); const insertCmdObj2 = makeInsertCmdObj({_id: -300, x: -300}); + const cmdObjToRetry = makeCmdObjToRetry(cmdObj); const insertCmdObjs = [insertCmdObj0, insertCmdObj1, insertCmdObj2]; + // Initial try. + const initialLsid = txnOptions.makeSessionIdFunc(); + let initialTxnNumber = 0; let initialRes; - if (txnOptions.oplogEntryLocation == kOplogEntryLocation.kLast) { - assert.commandWorked(mongosTestDB.runCommand(insertCmdObj0)); - assert.commandWorked(mongosTestDB.runCommand(insertCmdObj1)); - assert.commandWorked(mongosTestDB.runCommand(insertCmdObj2)); - initialRes = assert.commandWorked(mongosTestDB.runCommand(cmdObjToRetry)); - } else if (txnOptions.oplogEntryLocation == kOplogEntryLocation.kMiddle) { - assert.commandWorked(mongosTestDB.runCommand(insertCmdObj0)); - assert.commandWorked(mongosTestDB.runCommand(insertCmdObj1)); - initialRes = assert.commandWorked(mongosTestDB.runCommand(cmdObjToRetry)); - assert.commandWorked(mongosTestDB.runCommand(insertCmdObj2)); - } else { - assert.commandWorked(mongosTestDB.runCommand(insertCmdObj0)); - initialRes = assert.commandWorked(mongosTestDB.runCommand(cmdObjToRetry)); - assert.commandWorked(mongosTestDB.runCommand(insertCmdObj1)); - assert.commandWorked(mongosTestDB.runCommand(insertCmdObj2)); - } - commitTransaction(lsid, txnNumber, txnOptions.isPreparedTxn); + runTxnRetryOnLockTimeoutError(() => { + initialTxnNumber++; + setTxnFields(cmdObjToRetry, initialLsid, initialTxnNumber); + insertCmdObjs.forEach(cmdObj => setTxnFields(cmdObj, initialLsid, initialTxnNumber)); + if (txnOptions.oplogEntryLocation == kOplogEntryLocation.kLast) { + assert.commandWorked(mongosTestDB.runCommand(insertCmdObj0)); + assert.commandWorked(mongosTestDB.runCommand(insertCmdObj1)); + assert.commandWorked(mongosTestDB.runCommand(insertCmdObj2)); + initialRes = assert.commandWorked(mongosTestDB.runCommand(cmdObjToRetry)); + } else if (txnOptions.oplogEntryLocation == kOplogEntryLocation.kMiddle) { + assert.commandWorked(mongosTestDB.runCommand(insertCmdObj0)); + assert.commandWorked(mongosTestDB.runCommand(insertCmdObj1)); + initialRes = assert.commandWorked(mongosTestDB.runCommand(cmdObjToRetry)); + assert.commandWorked(mongosTestDB.runCommand(insertCmdObj2)); + } else { + assert.commandWorked(mongosTestDB.runCommand(insertCmdObj0)); + initialRes = assert.commandWorked(mongosTestDB.runCommand(cmdObjToRetry)); + assert.commandWorked(mongosTestDB.runCommand(insertCmdObj1)); + assert.commandWorked(mongosTestDB.runCommand(insertCmdObj2)); + } + commitTransaction(initialLsid, initialTxnNumber, txnOptions.isPreparedTxn); + }); - const txnStateBeforeRetry = getTransactionState(lsid, txnNumber); - assert.eq(txnStateBeforeRetry.oplogEntries.length, + const initialTxnStateBefore = getTransactionState(initialLsid, initialTxnNumber); + assert.eq(initialTxnStateBefore.oplogEntries.length, (txnOptions.isPreparedTxn ? insertCmdObjs.length + 1 : insertCmdObjs.length) + (expectFindAndModifyImageInOplog ? 1 : 0)); - assert.eq(txnStateBeforeRetry.imageEntries.length, + assert.eq(initialTxnStateBefore.imageEntries.length, expectFindAndModifyImageInSideCollection ? 1 : 0, - txnStateBeforeRetry.imageEntries); - assertConsistentImageEntries(lsid, txnNumber); + initialTxnStateBefore.imageEntries); + assertConsistentImageEntries(initialLsid, initialTxnNumber); setUpTestMode(testMode); - insertCmdObjs.forEach(insertCmdObj => { - const retryRes = assert.commandWorked(mongosTestDB.runCommand(insertCmdObj)); - assert.eq(retryRes.n, 1); + // Retry in the initial internal transaction. No need to commit since the transaction has + // already committed. + const retryRes = assert.commandWorked(mongosTestDB.runCommand(cmdObj)); + checkRetryResponseFunc(initialRes, retryRes); + + // Retry in a different internal transaction (running in an internal session with a + // different txnUUID) to simulate a retry from a different mongos. + const retryLsid = Object.assign({}, initialLsid, {txnUUID: UUID()}); + let retryTxnNumber = 0; + runTxnRetryOnLockTimeoutError(() => { + retryTxnNumber++; + setTxnFields(cmdObjToRetry, retryLsid, retryTxnNumber); + insertCmdObjs.forEach(cmdObj => setTxnFields(cmdObj, retryLsid, retryTxnNumber)); + insertCmdObjs.forEach(insertCmdObj => { + const retryRes = assert.commandWorked(mongosTestDB.runCommand(insertCmdObj)); + assert.eq(retryRes.n, 1); + }); + const retryRes = assert.commandWorked(mongosTestDB.runCommand(cmdObjToRetry)); + checkRetryResponseFunc(initialRes, retryRes); + commitTransaction( + retryLsid, retryTxnNumber, txnOptions.isPreparedTxn, true /* isRetry */); }); - const retryRes = assert.commandWorked(mongosTestDB.runCommand(cmdObjToRetry)); - checkFunc(initialRes, retryRes); - commitTransaction(lsid, txnNumber, txnOptions.isPreparedTxn, true /* isRetry */); - const txnStateAfterRetry = getTransactionState(lsid, txnNumber); - assert.eq(txnStateBeforeRetry.oplogEntries, txnStateAfterRetry.oplogEntries); - assert.eq(txnStateBeforeRetry.txnEntries, txnStateAfterRetry.txnEntries); - assert.eq(txnStateBeforeRetry.imageEntries, txnStateBeforeRetry.imageEntries); + const initialTxnStateAfter = getTransactionState(initialLsid, initialTxnNumber); + assert.eq(initialTxnStateBefore.oplogEntries, initialTxnStateAfter.oplogEntries); + assert.eq(initialTxnStateBefore.txnEntries, initialTxnStateAfter.txnEntries); + assert.eq(initialTxnStateBefore.imageEntries, initialTxnStateAfter.imageEntries); + // The retry should not generate any persisted transaction state. + const retryTxnState = getTransactionState(retryLsid, retryTxnNumber); + assert.eq(retryTxnState.oplogEntries.length, 0, retryTxnState); + assert.eq(retryTxnState.txnEntries.length, 0, retryTxnState); + assert.eq(retryTxnState.imageEntries.length, 0, retryTxnState); assert.commandWorked(mongosTestColl.remove({})); } - function testRetry(cmdObj, lsid, txnNumber, { + function testRetry(cmdObj, { + txnOptions, + testMode, expectRetryToSucceed, expectFindAndModifyImageInOplog, expectFindAndModifyImageInSideCollection, - txnOptions, - testMode, - checkFunc + checkRetryResponseFunc }) { - const testRetryFunc = txnOptions.isLargeTxn ? testRetryLargeTxn : testRetryBasic; - testRetryFunc(cmdObj, lsid, txnNumber, { + const testRetryFunc = (() => { + if (txnOptions.isLargeTxn) { + // This fixture only supports testing large retryable transactions since when a + // non-retryable transaction is retried, it fails before the it even starts so + // testing with a large transaction doesn't add any test coverage. + assert(expectRetryToSucceed); + return testRetryableLargeTxn; + } + return expectRetryToSucceed ? testRetryableBasic : testNonRetryableBasic; + })(); + testRetryFunc(cmdObj, { + txnOptions, + testMode, expectRetryToSucceed, expectFindAndModifyImageInOplog, expectFindAndModifyImageInSideCollection, - txnOptions, - testMode, - checkFunc + checkRetryResponseFunc }); } - function testRetryInserts(lsid, txnNumber, {expectRetryToSucceed, txnOptions, testMode}) { + function testRetryInserts({txnOptions, testMode, expectRetryToSucceed}) { jsTest.log("Testing batched inserts"); const insertCmdObj = { insert: kCollName, documents: [{_id: 0, x: 0}, {_id: 1, x: 1}], }; - const checkFunc = (initialRes, retryRes) => { + const checkRetryResponseFunc = (initialRes, retryRes) => { assert.eq(initialRes.n, retryRes.n); insertCmdObj.documents.forEach(doc => { assert.eq(mongosTestColl.count(doc), 1); }); }; - testRetry( - insertCmdObj, lsid, txnNumber, {expectRetryToSucceed, txnOptions, testMode, checkFunc}); + testRetry(insertCmdObj, + {txnOptions, testMode, expectRetryToSucceed, checkRetryResponseFunc}); } - function testRetryUpdates(lsid, txnNumber, {expectRetryToSucceed, txnOptions, testMode}) { + function testRetryUpdates({txnOptions, testMode, expectRetryToSucceed}) { jsTest.log("Testing batched updates"); assert.commandWorked(mongosTestColl.insert([{_id: 0, x: 0}, {_id: 1, x: 1}])); @@ -295,7 +396,7 @@ function RetryableInternalTransactionTest(collectionOptions = {}) { updates: [{q: {_id: 0, x: 0}, u: {$inc: {x: 10}}}, {q: {_id: 1, x: 1}, u: {$inc: {x: 10}}}], }; - const checkFunc = (initialRes, retryRes) => { + const checkRetryResponseFunc = (initialRes, retryRes) => { assert.eq(initialRes.nModified, retryRes.nModified); updateCmdObj.updates.forEach(updateArgs => { const originalDoc = updateArgs.q; @@ -305,11 +406,11 @@ function RetryableInternalTransactionTest(collectionOptions = {}) { assert.eq(mongosTestColl.count(updatedDoc), 1); }); }; - testRetry( - updateCmdObj, lsid, txnNumber, {expectRetryToSucceed, txnOptions, testMode, checkFunc}); + testRetry(updateCmdObj, + {txnOptions, testMode, expectRetryToSucceed, checkRetryResponseFunc}); } - function testRetryDeletes(lsid, txnNumber, {expectRetryToSucceed, txnOptions, testMode}) { + function testRetryDeletes({txnOptions, testMode, expectRetryToSucceed}) { jsTest.log("Testing batched deletes"); assert.commandWorked(mongosTestColl.insert([{_id: 0, x: 0}, {_id: 1, x: 1}])); @@ -318,50 +419,50 @@ function RetryableInternalTransactionTest(collectionOptions = {}) { delete: kCollName, deletes: [{q: {_id: 0, x: 0}, limit: 1}, {q: {_id: 1, x: 1}, limit: 1}], }; - const checkFunc = (initialRes, retryRes) => { + const checkRetryResponseFunc = (initialRes, retryRes) => { assert.eq(initialRes.n, retryRes.n); deleteCmdObj.deletes.forEach(deleteArgs => { assert.eq(mongosTestColl.count(deleteArgs.q), 0); }); }; - testRetry( - deleteCmdObj, lsid, txnNumber, {expectRetryToSucceed, txnOptions, testMode, checkFunc}); + testRetry(deleteCmdObj, + {txnOptions, testMode, expectRetryToSucceed, checkRetryResponseFunc}); } - function testRetryFindAndModify(findAndModifyCmdObj, lsid, txnNumber, { - expectRetryToSucceed, - expectFindAndModifyImage, + function testRetryFindAndModify(findAndModifyCmdObj, { txnOptions, testMode, enableFindAndModifyImageCollection, + expectRetryToSucceed, + expectFindAndModifyImage, }) { const shard0Primary = st.rs0.getPrimary(); assert.commandWorked(shard0Primary.adminCommand({ setParameter: 1, storeFindAndModifyImagesInSideCollection: enableFindAndModifyImageCollection })); - const checkFunc = (initialRes, retryRes) => { + const checkRetryResponseFunc = (initialRes, retryRes) => { assert.eq(initialRes.lastErrorObject, retryRes.lastErrorObject); assert.eq(initialRes.value, retryRes.value); }; - testRetry(findAndModifyCmdObj, lsid, txnNumber, { + testRetry(findAndModifyCmdObj, { + txnOptions, + testMode, expectRetryToSucceed, expectFindAndModifyImageInOplog: expectRetryToSucceed && expectFindAndModifyImage && !enableFindAndModifyImageCollection, expectFindAndModifyImageInSideCollection: expectRetryToSucceed && expectFindAndModifyImage && enableFindAndModifyImageCollection, - txnOptions, - testMode, - checkFunc + checkRetryResponseFunc }); } - function testRetryFindAndModifyUpsert(lsid, txnNumber, { - expectRetryToSucceed, + function testRetryFindAndModifyUpsert({ txnOptions, testMode, enableFindAndModifyImageCollection, + expectRetryToSucceed, }) { jsTest.log( "Testing findAndModify upsert (i.e. no preImage or postImage) with enableFindAndModifyImageCollection: " + @@ -374,20 +475,20 @@ function RetryableInternalTransactionTest(collectionOptions = {}) { upsert: true, }; const expectFindAndModifyImage = false; // no pre or post image. - testRetryFindAndModify(findAndModifyCmdObj, lsid, txnNumber, { - expectRetryToSucceed, - expectFindAndModifyImage, + testRetryFindAndModify(findAndModifyCmdObj, { txnOptions, testMode, enableFindAndModifyImageCollection, + expectFindAndModifyImage, + expectRetryToSucceed, }); } - function testRetryFindAndModifyUpdateWithPreImage(lsid, txnNumber, { - expectRetryToSucceed, + function testRetryFindAndModifyUpdateWithPreImage({ txnOptions, testMode, enableFindAndModifyImageCollection, + expectRetryToSucceed, }) { jsTest.log( "Testing findAndModify update with preImage with enableFindAndModifyImageCollection: " + @@ -400,20 +501,20 @@ function RetryableInternalTransactionTest(collectionOptions = {}) { update: {$inc: {x: -10}}, }; const expectFindAndModifyImage = true; - testRetryFindAndModify(findAndModifyCmdObj, lsid, txnNumber, { - expectRetryToSucceed, - expectFindAndModifyImage, + testRetryFindAndModify(findAndModifyCmdObj, { txnOptions, testMode, enableFindAndModifyImageCollection, + expectFindAndModifyImage, + expectRetryToSucceed, }); } - function testRetryFindAndModifyUpdateWithPostImage(lsid, txnNumber, { - expectRetryToSucceed, + function testRetryFindAndModifyUpdateWithPostImage({ txnOptions, testMode, enableFindAndModifyImageCollection, + expectRetryToSucceed, }) { jsTest.log( "Testing findAndModify update with postImage with enableFindAndModifyImageCollection: " + @@ -427,20 +528,20 @@ function RetryableInternalTransactionTest(collectionOptions = {}) { new: true, }; const expectFindAndModifyImage = true; - testRetryFindAndModify(findAndModifyCmdObj, lsid, txnNumber, { - expectRetryToSucceed, - expectFindAndModifyImage, + testRetryFindAndModify(findAndModifyCmdObj, { txnOptions, testMode, enableFindAndModifyImageCollection, + expectFindAndModifyImage, + expectRetryToSucceed, }); } - function testRetryFindAndModifyRemove(lsid, txnNumber, { - expectRetryToSucceed, + function testRetryFindAndModifyRemove({ txnOptions, testMode, enableFindAndModifyImageCollection, + expectRetryToSucceed, }) { jsTest.log( "Testing findAndModify remove (i.e. with preImage) with enableFindAndModifyImageCollection: " + @@ -453,84 +554,84 @@ function RetryableInternalTransactionTest(collectionOptions = {}) { remove: true, }; const expectFindAndModifyImage = true; - testRetryFindAndModify(findAndModifyCmdObj, lsid, txnNumber, { - expectRetryToSucceed, - enableFindAndModifyImageCollection, + testRetryFindAndModify(findAndModifyCmdObj, { txnOptions, testMode, + enableFindAndModifyImageCollection, expectFindAndModifyImage, + expectRetryToSucceed, }); } this.TestMode = kTestMode; - this.runInsertUpdateDeleteTests = function(lsid, testOptions) { - testOptions.lastUsedTxnNumber = - testOptions.lastUsedTxnNumber ? testOptions.lastUsedTxnNumber : 0; - testOptions.txnOptions = testOptions.txnOptions ? testOptions.txnOptions : {}; + this.runInsertUpdateDeleteTests = function(testOptions) { if (testOptions.txnOptions.isLargeTxn) { testOptions.txnOptions.oplogEntryLocation = getRandomOplogEntryLocation(); } jsTest.log(`Testing insert, update and delete with options: ${tojson(testOptions)}`); - testRetryInserts(lsid, testOptions.lastUsedTxnNumber++, testOptions); - testRetryUpdates(lsid, testOptions.lastUsedTxnNumber++, testOptions); - testRetryDeletes(lsid, testOptions.lastUsedTxnNumber++, testOptions); + testRetryInserts(testOptions); + testRetryUpdates(testOptions); + testRetryDeletes(testOptions); }; - function runFindAndModifyTests(lsid, testOptions) { - testOptions.lastUsedTxnNumber = - testOptions.lastUsedTxnNumber ? testOptions.lastUsedTxnNumber : 0; - testOptions.txnOptions = testOptions.txnOptions ? testOptions.txnOptions : {}; + function runFindAndModifyTests(testOptions) { if (testOptions.txnOptions.isLargeTxn) { testOptions.txnOptions.oplogEntryLocation = getRandomOplogEntryLocation(); } jsTest.log(`Testing findAndModify with options: ${tojson(testOptions)}`); - testRetryFindAndModifyUpsert(lsid, testOptions.lastUsedTxnNumber++, testOptions); - testRetryFindAndModifyUpdateWithPreImage( - lsid, testOptions.lastUsedTxnNumber++, testOptions); - testRetryFindAndModifyUpdateWithPostImage( - lsid, testOptions.lastUsedTxnNumber++, testOptions); - testRetryFindAndModifyRemove(lsid, testOptions.lastUsedTxnNumber++, testOptions); + testRetryFindAndModifyUpsert(testOptions); + testRetryFindAndModifyUpdateWithPreImage(testOptions); + testRetryFindAndModifyUpdateWithPostImage(testOptions); + testRetryFindAndModifyRemove(testOptions); } - this.runFindAndModifyTestsEnableImageCollection = function(lsid, testOptions) { + this.runFindAndModifyTestsEnableImageCollection = function(testOptions) { testOptions.enableFindAndModifyImageCollection = true; - runFindAndModifyTests(lsid, testOptions); + runFindAndModifyTests(testOptions); }; - this.runFindAndModifyTestsDisableImageCollection = function(lsid, testOptions) { + this.runFindAndModifyTestsDisableImageCollection = function(testOptions) { testOptions.enableFindAndModifyImageCollection = false; - runFindAndModifyTests(lsid, testOptions); + runFindAndModifyTests(testOptions); }; this.runTestsForAllUnpreparedRetryableInternalTransactionTypes = function(runTestsFunc, testMode) { + const makeSessionIdFunc = makeSessionIdForRetryableInternalTransaction; const expectRetryToSucceed = true; - runTestsFunc(makeSessionIdForRetryableInternalTransaction(), { - expectRetryToSucceed, - txnOptions: {isPreparedTxn: false, isLargeTxn: false}, - testMode + runTestsFunc({ + txnOptions: {makeSessionIdFunc, isPreparedTxn: false, isLargeTxn: false}, + testMode, + expectRetryToSucceed }); - runTestsFunc( - makeSessionIdForRetryableInternalTransaction(), - {expectRetryToSucceed, txnOptions: {isPreparedTxn: false, isLargeTxn: true}, testMode}); + runTestsFunc({ + txnOptions: {makeSessionIdFunc, isPreparedTxn: false, isLargeTxn: true}, + testMode, + expectRetryToSucceed + }); }; this.runTestsForAllPreparedRetryableInternalTransactionTypes = function(runTestsFunc, testMode) { + const makeSessionIdFunc = makeSessionIdForRetryableInternalTransaction; const expectRetryToSucceed = true; - runTestsFunc( - makeSessionIdForRetryableInternalTransaction(), - {expectRetryToSucceed, txnOptions: {isPreparedTxn: true, isLargeTxn: false}, testMode}); + runTestsFunc({ + txnOptions: {makeSessionIdFunc, isPreparedTxn: true, isLargeTxn: false}, + testMode, + expectRetryToSucceed + }); - runTestsFunc( - makeSessionIdForRetryableInternalTransaction(), - {expectRetryToSucceed, txnOptions: {isPreparedTxn: true, isLargeTxn: true}, testMode}); + runTestsFunc({ + txnOptions: {makeSessionIdFunc, isPreparedTxn: true, isLargeTxn: true}, + testMode, + expectRetryToSucceed + }); }; this.runTestsForAllRetryableInternalTransactionTypes = function(runTestsFunc, testMode) { diff --git a/jstests/sharding/internal_txns/overwrite_txns.js b/jstests/sharding/internal_txns/overwrite_txns.js new file mode 100644 index 00000000000..baf7abd7931 --- /dev/null +++ b/jstests/sharding/internal_txns/overwrite_txns.js @@ -0,0 +1,292 @@ +/* + * Tests when internal transactions overwrite existing transactions. + * + * @tags: [requires_fcv_60, uses_transactions] + */ +(function() { +'use strict'; + +load("jstests/libs/fail_point_util.js"); +load("jstests/libs/uuid_util.js"); + +const st = new ShardingTest({shards: 1, rs: {nodes: 2}}); + +const kDbName = "testDb"; +const kCollName = "testColl"; +const testDB = st.rs0.getPrimary().getDB(kDbName); +assert.commandWorked(testDB[kCollName].insert({x: 1})); // Set up the collection. + +(() => { + jsTest.log("Verify in progress child transactions are aborted by higher txnNumbers"); + + let clientTxnNumber = 5; + const clientSession = {id: UUID()}; + const retryableChildSession = { + id: clientSession.id, + txnUUID: UUID(), + txnNumber: NumberLong(clientTxnNumber) + }; + const nonRetryableChildSession = {id: clientSession.id, txnUUID: UUID()}; + + assert.commandWorked(testDB.runCommand({ + insert: kCollName, + documents: [{x: 1}], + lsid: clientSession, + txnNumber: NumberLong(clientTxnNumber), + startTransaction: true, + autocommit: false + })); + + // A new child transaction should abort an existing client transaction. + clientTxnNumber++; + retryableChildSession.txnNumber = NumberLong(clientTxnNumber); + assert.commandWorked(testDB.runCommand({ + insert: kCollName, + documents: [{x: 1}], + lsid: retryableChildSession, + txnNumber: NumberLong(0), + startTransaction: true, + autocommit: false + })); + // The client transaction should have been aborted. + assert.commandFailedWithCode(testDB.adminCommand({ + commitTransaction: 1, + lsid: clientSession, + txnNumber: NumberLong(clientTxnNumber - 1), + autocommit: false + }), + ErrorCodes.TransactionTooOld); + + // A non-retryable child transaction shouldn't affect retryable operations. + assert.commandWorked(testDB.runCommand({ + insert: kCollName, + documents: [{x: 1}], + lsid: nonRetryableChildSession, + txnNumber: NumberLong(0), + startTransaction: true, + autocommit: false + })); + // The retryable child transaction should still be open. + assert.commandWorked(testDB.runCommand({ + find: kCollName, + lsid: retryableChildSession, + txnNumber: NumberLong(0), + autocommit: false + })); + + // A new child transaction should abort a lower child transaction. + clientTxnNumber++; + let retryableChildSessionCopy = Object.merge({}, retryableChildSession); + retryableChildSession.txnNumber = NumberLong(clientTxnNumber); + assert.commandWorked(testDB.runCommand({ + insert: kCollName, + documents: [{x: 1}], + lsid: retryableChildSession, + txnNumber: NumberLong(0), + startTransaction: true, + autocommit: false + })); + // The child transaction should have been aborted. + assert.commandFailedWithCode(testDB.adminCommand({ + commitTransaction: 1, + lsid: retryableChildSessionCopy, + txnNumber: NumberLong(0), + autocommit: false + }), + ErrorCodes.TransactionTooOld); + + // A new client transaction should abort a lower child transaction. + clientTxnNumber++; + assert.commandWorked(testDB.runCommand({ + insert: kCollName, + documents: [{x: 1}], + lsid: clientSession, + txnNumber: NumberLong(clientTxnNumber), + startTransaction: true, + autocommit: false + })); + // The client transaction should have been aborted. + assert.commandFailedWithCode(testDB.adminCommand({ + commitTransaction: 1, + lsid: retryableChildSessionCopy, + txnNumber: NumberLong(0), + autocommit: false + }), + ErrorCodes.TransactionTooOld); + + // A new retryable write should abort a lower child transaction. + clientTxnNumber++; + retryableChildSession.txnNumber = NumberLong(clientTxnNumber); + assert.commandWorked(testDB.runCommand({ + insert: kCollName, + documents: [{x: 1}], + lsid: retryableChildSession, + txnNumber: NumberLong(0), + startTransaction: true, + autocommit: false + })); + clientTxnNumber++; + assert.commandWorked(testDB.runCommand({ + insert: kCollName, + documents: [{x: 1}], + lsid: clientSession, + txnNumber: NumberLong(clientTxnNumber) + })); + // The child transaction should have been aborted. + assert.commandFailedWithCode(testDB.adminCommand({ + commitTransaction: 1, + lsid: retryableChildSession, + txnNumber: NumberLong(0), + autocommit: false + }), + ErrorCodes.TransactionTooOld); + + // The non-retryable child transaction should still be open. + assert.commandWorked(testDB.adminCommand({ + commitTransaction: 1, + lsid: nonRetryableChildSession, + txnNumber: NumberLong(0), + autocommit: false + })); +})(); + +(() => { + jsTest.log("Verify prepared child transactions are not aborted by higher txnNumbers"); + + let clientTxnNumber = 5; + const clientSession = {id: UUID()}; + const retryableChildSession = { + id: clientSession.id, + txnUUID: UUID(), + txnNumber: NumberLong(clientTxnNumber) + }; + const nonRetryableChildSession = {id: clientSession.id, txnUUID: UUID()}; + + // Prepare a retryable and non-retryable child transaction. + + assert.commandWorked(testDB.runCommand({ + insert: kCollName, + documents: [{x: 1}], + lsid: nonRetryableChildSession, + txnNumber: NumberLong(0), + startTransaction: true, + autocommit: false + })); + assert.commandWorked(testDB.adminCommand({ + prepareTransaction: 1, + lsid: nonRetryableChildSession, + txnNumber: NumberLong(0), + autocommit: false + })); + + assert.commandWorked(testDB.runCommand({ + insert: kCollName, + documents: [{x: 1}], + lsid: retryableChildSession, + txnNumber: NumberLong(0), + startTransaction: true, + autocommit: false + })); + assert.commandWorked(testDB.adminCommand({ + prepareTransaction: 1, + lsid: retryableChildSession, + txnNumber: NumberLong(0), + autocommit: false + })); + + // Verify a higher txnNumber cannot be accepted until the retryable transaction exits prepare. + // Test all three sources of a higher txnNumber: client retryable write, client transaction, and + // a retryable child session transaction. + clientTxnNumber++; + assert.commandFailedWithCode(testDB.runCommand({ + insert: kCollName, + documents: [{x: 1}], + lsid: clientSession, + txnNumber: NumberLong(clientTxnNumber), + maxTimeMS: 1000 + }), + ErrorCodes.MaxTimeMSExpired); + + clientTxnNumber++; + assert.commandFailedWithCode(testDB.runCommand({ + insert: kCollName, + documents: [{x: 1}], + lsid: clientSession, + txnNumber: NumberLong(clientTxnNumber), + startTransaction: true, + autocommit: false, + maxTimeMS: 1000 + }), + ErrorCodes.MaxTimeMSExpired); + + clientTxnNumber++; + assert.commandFailedWithCode(testDB.runCommand({ + insert: kCollName, + documents: [{x: 1}], + lsid: {id: clientSession.id, txnUUID: UUID(), txnNumber: NumberLong(clientTxnNumber)}, + txnNumber: NumberLong(clientTxnNumber), + startTransaction: true, + autocommit: false, + maxTimeMS: 1000 + }), + ErrorCodes.MaxTimeMSExpired); + + // Verify a transaction blocked on a prepared child transaction can become unstuck and succeed + // once the child transaction exits prepare. + const fp = configureFailPoint( + st.rs0.getPrimary(), + "waitAfterNewStatementBlocksBehindOpenInternalTransactionForRetryableWrite"); + const newTxnThread = new Thread((host, lsidUUID, txnNumber) => { + const lsid = {id: UUID(lsidUUID)}; + + const conn = new Mongo(host); + assert.commandWorked(conn.getDB("foo").runCommand({ + insert: "test", + documents: [{x: 1}], + lsid: lsid, + txnNumber: NumberLong(txnNumber), + startTransaction: true, + autocommit: false, + })); + assert.commandWorked(conn.adminCommand({ + commitTransaction: 1, + lsid: lsid, + txnNumber: NumberLong(txnNumber), + autocommit: false + })); + }, st.s.host, extractUUIDFromObject(clientSession.id), clientTxnNumber); + newTxnThread.start(); + + // Wait for the side transaction to hit a PreparedTransactionInProgress error, then resolve the + // prepared transaction and verify the side transaction can successfully complete. + fp.wait(); + fp.off(); + + assert.commandWorked(testDB.adminCommand({ + abortTransaction: 1, + lsid: retryableChildSession, + txnNumber: NumberLong(0), + autocommit: false + })); + + newTxnThread.join(); + + // A higher txnNumber is accepted despite the prepared non-retryable child transaction. + clientTxnNumber++; + assert.commandWorked(testDB.runCommand({ + insert: kCollName, + documents: [{x: 1}], + lsid: clientSession, + txnNumber: NumberLong(clientTxnNumber) + })); + + assert.commandWorked(testDB.adminCommand({ + abortTransaction: 1, + lsid: nonRetryableChildSession, + txnNumber: NumberLong(0), + autocommit: false + })); +})(); + +st.stop(); +})(); diff --git a/jstests/sharding/internal_txns/partial_index.js b/jstests/sharding/internal_txns/partial_index.js index 032505660ab..b9c5462aaa4 100644 --- a/jstests/sharding/internal_txns/partial_index.js +++ b/jstests/sharding/internal_txns/partial_index.js @@ -9,242 +9,327 @@ load("jstests/libs/analyze_plan.js"); -const st = new ShardingTest({shards: {rs0: {nodes: 2}}}); - const kDbName = "testDb"; const kCollName = "testColl"; const kConfigTxnNs = "config.transactions"; +const kPartialIndexName = "parent_lsid"; + +function runTest(st, alwaysCreateFeatureFlagEnabled) { + const mongosTestDB = st.s.getDB(kDbName); + const shard0PrimaryConfigTxnColl = st.rs0.getPrimary().getCollection(kConfigTxnNs); + + function assertPartialIndexExists(node) { + const configDB = node.getDB("config"); + const indexSpecs = + assert.commandWorked(configDB.runCommand({"listIndexes": "transactions"})) + .cursor.firstBatch; + indexSpecs.sort((index0, index1) => index0.name > index1.name); + assert.eq(indexSpecs.length, 2); + const idIndexSpec = indexSpecs[0]; + assert.eq(idIndexSpec.key, {"_id": 1}); + const partialIndexSpec = indexSpecs[1]; + assert.eq(partialIndexSpec.key, {"parentLsid": 1, "_id.txnNumber": 1, "_id": 1}); + assert.eq(partialIndexSpec.partialFilterExpression, {"parentLsid": {"$exists": true}}); + } + + function assertFindUsesCoveredQuery(node) { + const configTxnColl = node.getCollection(kConfigTxnNs); + const childSessionDoc = configTxnColl.findOne({ + "_id.id": sessionUUID, + "_id.txnNumber": childLsid.txnNumber, + "_id.txnUUID": childLsid.txnUUID + }); + + const explainRes = assert.commandWorked( + configTxnColl.explain() + .find({"parentLsid": parentSessionDoc._id, "_id.txnNumber": childLsid.txnNumber}, + {_id: 1}) + .finish()); + const winningPlan = getWinningPlan(explainRes.queryPlanner); + assert.eq(winningPlan.stage, "PROJECTION_COVERED"); + assert.eq(winningPlan.inputStage.stage, "IXSCAN"); + + const findRes = + configTxnColl + .find({"parentLsid": parentSessionDoc._id, "_id.txnNumber": childLsid.txnNumber}, + {_id: 1}) + .toArray(); + assert.eq(findRes.length, 1); + assert.eq(findRes[0]._id, childSessionDoc._id); + } + + function assertPartialIndexDoesNotExist(node) { + const configDB = node.getDB("config"); + const indexSpecs = + assert.commandWorked(configDB.runCommand({"listIndexes": "transactions"})) + .cursor.firstBatch; + assert.eq(indexSpecs.length, 1); + const idIndexSpec = indexSpecs[0]; + assert.eq(idIndexSpec.key, {"_id": 1}); + } + + function indexRecreationTest(expectRecreateAfterDrop) { + st.rs0.getPrimary().getCollection(kConfigTxnNs).dropIndex(kPartialIndexName); + st.rs0.awaitReplication(); + + st.rs0.nodes.forEach(node => { + assertPartialIndexDoesNotExist(node); + }); + + let primary = st.rs0.getPrimary(); + assert.commandWorked( + primary.adminCommand({replSetStepDown: ReplSetTest.kForeverSecs, force: true})); + assert.commandWorked(primary.adminCommand({replSetFreeze: 0})); + + st.rs0.awaitNodesAgreeOnPrimary(); + st.rs0.awaitReplication(); + + st.rs0.nodes.forEach(node => { + if (expectRecreateAfterDrop) { + assertPartialIndexExists(node); + } else { + assertPartialIndexDoesNotExist(node); + } + }); + } + + // If the collection is empty and the index does not exist, we should always create the partial + // index on stepup, + indexRecreationTest(true /* expectRecreateAfterDrop */); + + const sessionUUID = UUID(); + const parentLsid = {id: sessionUUID}; + const parentTxnNumber = 35; + let stmtId = 0; -const mongosTestDB = st.s.getDB(kDbName); -const shard0PrimaryConfigTxnColl = st.rs0.getPrimary().getCollection(kConfigTxnNs); - -const sessionUUID = UUID(); -const parentLsid = { - id: sessionUUID -}; -const parentTxnNumber = 35; -let stmtId = 0; - -assert.commandWorked(mongosTestDB.runCommand({ - insert: kCollName, - documents: [{_id: 0}], - lsid: parentLsid, - txnNumber: NumberLong(parentTxnNumber), - stmtId: NumberInt(stmtId++) -})); -const parentSessionDoc = shard0PrimaryConfigTxnColl.findOne({"_id.id": sessionUUID}); - -const childLsid = { - id: sessionUUID, - txnNumber: NumberLong(parentTxnNumber), - txnUUID: UUID() -}; -let childTxnNumber = 0; - -function runRetryableInternalTransaction(txnNumber) { assert.commandWorked(mongosTestDB.runCommand({ insert: kCollName, + documents: [{_id: 0}], + lsid: parentLsid, + txnNumber: NumberLong(parentTxnNumber), + stmtId: NumberInt(stmtId++) + })); + const parentSessionDoc = shard0PrimaryConfigTxnColl.findOne({"_id.id": sessionUUID}); + + const childLsid = {id: sessionUUID, txnNumber: NumberLong(parentTxnNumber), txnUUID: UUID()}; + let childTxnNumber = 0; + + function runRetryableInternalTransaction(txnNumber) { + assert.commandWorked(mongosTestDB.runCommand({ + insert: kCollName, + documents: [{x: 1}], + lsid: childLsid, + txnNumber: NumberLong(txnNumber), + stmtId: NumberInt(stmtId++), + autocommit: false, + startTransaction: true + })); + assert.commandWorked(mongosTestDB.adminCommand({ + commitTransaction: 1, + lsid: childLsid, + txnNumber: NumberLong(txnNumber), + autocommit: false + })); + } + + runRetryableInternalTransaction(childTxnNumber); + assert.eq(shard0PrimaryConfigTxnColl.count({"_id.id": sessionUUID}), 2); + + st.rs0.nodes.forEach(node => { + assertPartialIndexExists(node); + assertFindUsesCoveredQuery(node); + }); + + childTxnNumber++; + runRetryableInternalTransaction(childTxnNumber); + assert.eq(shard0PrimaryConfigTxnColl.count({"_id.id": sessionUUID}), 2); + + st.rs0.nodes.forEach(node => { + assertPartialIndexExists(node); + assertFindUsesCoveredQuery(node); + }); + + // + // Verify clients can create the index only if they provide the exact specification and that + // operations requiring the index fails if it does not exist. + // + + const indexConn = st.rs0.getPrimary(); + assert.commandWorked( + indexConn.getCollection("config.transactions").dropIndex(kPartialIndexName)); + + // Normal writes don't involve config.transactions, so they succeed. + assert.commandWorked(indexConn.getDB(kDbName).runCommand( + {insert: kCollName, documents: [{x: 1}], lsid: {id: UUID()}})); + + // Retryable writes read from the partial index, so they fail. + let res = assert.commandFailedWithCode(indexConn.getDB(kDbName).runCommand({ + insert: kCollName, + documents: [{x: 1}], + lsid: {id: UUID()}, + txnNumber: NumberLong(11) + }), + ErrorCodes.BadValue); + assert(res.errmsg.includes("Please create an index directly "), tojson(res)); + + // User transactions read from the partial index, so they fail. + assert.commandFailedWithCode(indexConn.getDB(kDbName).runCommand({ + insert: kCollName, + documents: [{x: 1}], + lsid: {id: UUID()}, + txnNumber: NumberLong(11), + startTransaction: true, + autocommit: false + }), + ErrorCodes.BadValue); + + // Non retryable internal transactions do not read from or update the partial index, so they can + // succeed without the index existing. + let nonRetryableTxnSession = {id: UUID(), txnUUID: UUID()}; + assert.commandWorked(indexConn.getDB(kDbName).runCommand({ + insert: kCollName, + documents: [{x: 1}], + lsid: nonRetryableTxnSession, + txnNumber: NumberLong(11), + stmtId: NumberInt(0), + startTransaction: true, + autocommit: false + })); + assert.commandWorked(indexConn.adminCommand({ + commitTransaction: 1, + lsid: nonRetryableTxnSession, + txnNumber: NumberLong(11), + autocommit: false + })); + + // Retryable transactions read from the partial index, so they fail. + assert.commandFailedWithCode(indexConn.getDB(kDbName).runCommand({ + insert: kCollName, + documents: [{x: 1}], + lsid: {id: UUID(), txnUUID: UUID(), txnNumber: NumberLong(2)}, + txnNumber: NumberLong(11), + stmtId: NumberInt(0), + startTransaction: true, + autocommit: false + }), + ErrorCodes.BadValue); + + // Recreating the partial index requires the exact options used internally, but in any order. + assert.commandFailedWithCode(indexConn.getDB("config").runCommand({ + createIndexes: "transactions", + indexes: [{v: 2, name: "parent_lsid", key: {parentLsid: 1, "_id.txnNumber": 1, _id: 1}}], + }), + ErrorCodes.IllegalOperation); + assert.commandWorked(indexConn.getDB("config").runCommand({ + createIndexes: "transactions", + indexes: [{ + name: "parent_lsid", + key: {parentLsid: 1, "_id.txnNumber": 1, _id: 1}, + partialFilterExpression: {parentLsid: {$exists: true}}, + v: 2, + }], + })); + + // Operations involving the index should succeed now. + + assert.commandWorked(indexConn.getDB(kDbName).runCommand( + {insert: kCollName, documents: [{x: 1}], lsid: {id: UUID()}})); + + assert.commandWorked(indexConn.getDB(kDbName).runCommand( + {insert: kCollName, documents: [{x: 1}], lsid: {id: UUID()}, txnNumber: NumberLong(11)})); + + let userSessionAfter = {id: UUID()}; + assert.commandWorked(indexConn.getDB(kDbName).runCommand({ + insert: kCollName, + documents: [{x: 1}], + lsid: userSessionAfter, + txnNumber: NumberLong(11), + startTransaction: true, + autocommit: false + })); + assert.commandWorked(indexConn.adminCommand({ + commitTransaction: 1, + lsid: userSessionAfter, + txnNumber: NumberLong(11), + autocommit: false + })); + + let nonRetryableTxnSessionAfter = {id: UUID(), txnUUID: UUID()}; + assert.commandWorked(indexConn.getDB(kDbName).runCommand({ + insert: kCollName, + documents: [{x: 1}], + lsid: nonRetryableTxnSessionAfter, + txnNumber: NumberLong(11), + stmtId: NumberInt(0), + startTransaction: true, + autocommit: false + })); + assert.commandWorked(indexConn.adminCommand({ + commitTransaction: 1, + lsid: nonRetryableTxnSessionAfter, + txnNumber: NumberLong(11), + autocommit: false + })); + + let retryableTxnSessionAfter = {id: UUID(), txnUUID: UUID(), txnNumber: NumberLong(2)}; + assert.commandWorked(indexConn.getDB(kDbName).runCommand({ + insert: kCollName, documents: [{x: 1}], - lsid: childLsid, - txnNumber: NumberLong(txnNumber), - stmtId: NumberInt(stmtId++), - autocommit: false, - startTransaction: true + lsid: retryableTxnSessionAfter, + txnNumber: NumberLong(11), + stmtId: NumberInt(0), + startTransaction: true, + autocommit: false })); - assert.commandWorked(mongosTestDB.adminCommand({ + assert.commandWorked(indexConn.adminCommand({ commitTransaction: 1, - lsid: childLsid, - txnNumber: NumberLong(txnNumber), + lsid: retryableTxnSessionAfter, + txnNumber: NumberLong(11), autocommit: false })); + + if (!alwaysCreateFeatureFlagEnabled) { + // We expect that if the partial index is dropped when the collection isn't empty, then on + // stepup we should not recreate the collection. + indexRecreationTest(false /* expectRecreateAfterDrop */); + } else { + // Creating the partial index when the collection isn't empty can be enabled by a feature + // flag. + indexRecreationTest(true /* expectRecreateAfterDrop */); + } } -function assertPartialIndexExists(node) { - const configDB = node.getDB("config"); - const indexSpecs = assert.commandWorked(configDB.runCommand({"listIndexes": "transactions"})) - .cursor.firstBatch; - indexSpecs.sort((index0, index1) => index0.name > index1.name); - assert.eq(indexSpecs.length, 2); - const idIndexSpec = indexSpecs[0]; - assert.eq(idIndexSpec.key, {"_id": 1}); - const partialIndexSpec = indexSpecs[1]; - assert.eq(partialIndexSpec.key, {"parentLsid": 1, "_id.txnNumber": 1, "_id": 1}); - assert.eq(partialIndexSpec.partialFilterExpression, {"parentLsid": {"$exists": true}}); +{ + const st = new ShardingTest({shards: {rs0: {nodes: 2}}}); + runTest(st, false /* alwaysCreateFeatureFlagEnabled */); + st.stop(); } -function assertFindUsesCoveredQuery(node) { - const configTxnColl = node.getCollection(kConfigTxnNs); - const childSessionDoc = configTxnColl.findOne({ - "_id.id": sessionUUID, - "_id.txnNumber": childLsid.txnNumber, - "_id.txnUUID": childLsid.txnUUID +{ + const featureFlagSt = new ShardingTest({ + shards: 1, + other: { + rs: {nodes: 2}, + rsOptions: + {setParameter: "featureFlagAlwaysCreateConfigTransactionsPartialIndexOnStepUp=true"} + } }); - const explainRes = assert.commandWorked( - configTxnColl.explain() - .find({"parentLsid": parentSessionDoc._id, "_id.txnNumber": childLsid.txnNumber}, - {_id: 1}) - .finish()); - const winningPlan = getWinningPlan(explainRes.queryPlanner); - assert.eq(winningPlan.stage, "PROJECTION_COVERED"); - assert.eq(winningPlan.inputStage.stage, "IXSCAN"); - - const findRes = - configTxnColl - .find({"parentLsid": parentSessionDoc._id, "_id.txnNumber": childLsid.txnNumber}, - {_id: 1}) - .toArray(); - assert.eq(findRes.length, 1); - assert.eq(findRes[0]._id, childSessionDoc._id); -} + // Sanity check the feature flag was enabled. + assert(assert + .commandWorked(featureFlagSt.rs0.getPrimary().adminCommand({ + getParameter: 1, + featureFlagAlwaysCreateConfigTransactionsPartialIndexOnStepUp: 1 + })) + .featureFlagAlwaysCreateConfigTransactionsPartialIndexOnStepUp.value); + assert(assert + .commandWorked(featureFlagSt.rs0.getSecondary().adminCommand({ + getParameter: 1, + featureFlagAlwaysCreateConfigTransactionsPartialIndexOnStepUp: 1 + })) + .featureFlagAlwaysCreateConfigTransactionsPartialIndexOnStepUp.value); -runRetryableInternalTransaction(childTxnNumber); -assert.eq(shard0PrimaryConfigTxnColl.count({"_id.id": sessionUUID}), 2); - -st.rs0.nodes.forEach(node => { - assertPartialIndexExists(node); - assertFindUsesCoveredQuery(node); -}); - -childTxnNumber++; -runRetryableInternalTransaction(childTxnNumber); -assert.eq(shard0PrimaryConfigTxnColl.count({"_id.id": sessionUUID}), 2); - -st.rs0.nodes.forEach(node => { - assertPartialIndexExists(node); - assertFindUsesCoveredQuery(node); -}); - -// -// Verify clients can create the index only if they provide the exact specification and that -// operations requiring the index fails if it does not exist. -// - -const indexConn = st.rs0.getPrimary(); -assert.commandWorked(indexConn.getCollection("config.transactions").dropIndex("parent_lsid")); - -// Normal writes don't involve config.transactions, so they succeed. -assert.commandWorked(indexConn.getDB(kDbName).runCommand( - {insert: kCollName, documents: [{x: 1}], lsid: {id: UUID()}})); - -// Retryable writes read from the partial index, so they fail. -let res = assert.commandFailedWithCode( - indexConn.getDB(kDbName).runCommand( - {insert: kCollName, documents: [{x: 1}], lsid: {id: UUID()}, txnNumber: NumberLong(11)}), - ErrorCodes.BadValue); -assert(res.errmsg.includes("Please create an index directly "), tojson(res)); - -// User transactions read from the partial index, so they fail. -assert.commandFailedWithCode(indexConn.getDB(kDbName).runCommand({ - insert: kCollName, - documents: [{x: 1}], - lsid: {id: UUID()}, - txnNumber: NumberLong(11), - startTransaction: true, - autocommit: false -}), - ErrorCodes.BadValue); - -// Non retryable internal transactions do not read from or update the partial index, so they can -// succeed without the index existing. -let nonRetryableTxnSession = {id: UUID(), txnUUID: UUID()}; -assert.commandWorked(indexConn.getDB(kDbName).runCommand({ - insert: kCollName, - documents: [{x: 1}], - lsid: nonRetryableTxnSession, - txnNumber: NumberLong(11), - stmtId: NumberInt(0), - startTransaction: true, - autocommit: false -})); -assert.commandWorked(indexConn.adminCommand({ - commitTransaction: 1, - lsid: nonRetryableTxnSession, - txnNumber: NumberLong(11), - autocommit: false -})); - -// Retryable transactions read from the partial index, so they fail. -assert.commandFailedWithCode(indexConn.getDB(kDbName).runCommand({ - insert: kCollName, - documents: [{x: 1}], - lsid: {id: UUID(), txnUUID: UUID(), txnNumber: NumberLong(2)}, - txnNumber: NumberLong(11), - stmtId: NumberInt(0), - startTransaction: true, - autocommit: false -}), - ErrorCodes.BadValue); - -// Recreating the partial index requires the exact options used internally, but in any order. -assert.commandFailedWithCode(indexConn.getDB("config").runCommand({ - createIndexes: "transactions", - indexes: [{v: 2, name: "parent_lsid", key: {parentLsid: 1, "_id.txnNumber": 1, _id: 1}}], -}), - ErrorCodes.IllegalOperation); -assert.commandWorked(indexConn.getDB("config").runCommand({ - createIndexes: "transactions", - indexes: [{ - name: "parent_lsid", - key: {parentLsid: 1, "_id.txnNumber": 1, _id: 1}, - partialFilterExpression: {parentLsid: {$exists: true}}, - v: 2, - }], -})); - -// Operations involving the index should succeed now. - -assert.commandWorked(indexConn.getDB(kDbName).runCommand( - {insert: kCollName, documents: [{x: 1}], lsid: {id: UUID()}})); - -assert.commandWorked(indexConn.getDB(kDbName).runCommand( - {insert: kCollName, documents: [{x: 1}], lsid: {id: UUID()}, txnNumber: NumberLong(11)})); - -let userSessionAfter = {id: UUID()}; -assert.commandWorked(indexConn.getDB(kDbName).runCommand({ - insert: kCollName, - documents: [{x: 1}], - lsid: userSessionAfter, - txnNumber: NumberLong(11), - startTransaction: true, - autocommit: false -})); -assert.commandWorked(indexConn.adminCommand( - {commitTransaction: 1, lsid: userSessionAfter, txnNumber: NumberLong(11), autocommit: false})); - -let nonRetryableTxnSessionAfter = {id: UUID(), txnUUID: UUID()}; -assert.commandWorked(indexConn.getDB(kDbName).runCommand({ - insert: kCollName, - documents: [{x: 1}], - lsid: nonRetryableTxnSessionAfter, - txnNumber: NumberLong(11), - stmtId: NumberInt(0), - startTransaction: true, - autocommit: false -})); -assert.commandWorked(indexConn.adminCommand({ - commitTransaction: 1, - lsid: nonRetryableTxnSessionAfter, - txnNumber: NumberLong(11), - autocommit: false -})); - -let retryableTxnSessionAfter = {id: UUID(), txnUUID: UUID(), txnNumber: NumberLong(2)}; -assert.commandWorked(indexConn.getDB(kDbName).runCommand({ - insert: kCollName, - documents: [{x: 1}], - lsid: retryableTxnSessionAfter, - txnNumber: NumberLong(11), - stmtId: NumberInt(0), - startTransaction: true, - autocommit: false -})); -assert.commandWorked(indexConn.adminCommand({ - commitTransaction: 1, - lsid: retryableTxnSessionAfter, - txnNumber: NumberLong(11), - autocommit: false -})); - -st.stop(); + runTest(featureFlagSt, true /* alwaysCreateFeatureFlagEnabled */); + featureFlagSt.stop(); +} })(); diff --git a/jstests/sharding/internal_txns/retryable_findAndModify_basic.js b/jstests/sharding/internal_txns/retryable_findAndModify_basic.js index 7221e192ac0..09f4d90054e 100644 --- a/jstests/sharding/internal_txns/retryable_findAndModify_basic.js +++ b/jstests/sharding/internal_txns/retryable_findAndModify_basic.js @@ -13,16 +13,22 @@ const transactionTest = new RetryableInternalTransactionTest(); { jsTest.log("Test that non-internal transactions cannot be retried"); - const lsid = {id: UUID()}; - const testOptions = {expectRetryToSucceed: false}; - transactionTest.runFindAndModifyTestsEnableImageCollection(lsid, testOptions); + const makeSessionIdFunc = () => { + return {id: UUID()}; + }; + const expectRetryToSucceed = false; + transactionTest.runFindAndModifyTestsEnableImageCollection( + {txnOptions: {makeSessionIdFunc}, expectRetryToSucceed}); } { jsTest.log("Test that non-retryable internal transactions cannot be retried"); - const lsid = {id: UUID(), txnUUID: UUID()}; - const testOptions = {expectRetryToSucceed: false}; - transactionTest.runFindAndModifyTestsEnableImageCollection(lsid, testOptions); + const makeSessionIdFunc = () => { + return {id: UUID(), txnUUID: UUID()}; + }; + const expectRetryToSucceed = false; + transactionTest.runFindAndModifyTestsEnableImageCollection( + {txnOptions: {makeSessionIdFunc}, expectRetryToSucceed}); } { diff --git a/jstests/sharding/internal_txns/retryable_writes_basic.js b/jstests/sharding/internal_txns/retryable_writes_basic.js index d3ebdadce81..666bf93416a 100644 --- a/jstests/sharding/internal_txns/retryable_writes_basic.js +++ b/jstests/sharding/internal_txns/retryable_writes_basic.js @@ -17,16 +17,22 @@ const transactionTest = new RetryableInternalTransactionTest(); { jsTest.log("Test that non-internal transactions cannot be retried"); - const lsid = {id: UUID()}; - const testOptions = {expectRetryToSucceed: false}; - transactionTest.runInsertUpdateDeleteTests(lsid, testOptions); + const makeSessionIdFunc = () => { + return {id: UUID()}; + }; + const expectRetryToSucceed = false; + transactionTest.runInsertUpdateDeleteTests( + {txnOptions: {makeSessionIdFunc}, expectRetryToSucceed}); } { jsTest.log("Test that non-retryable internal transactions cannot be retried"); - const lsid = {id: UUID(), txnUUID: UUID()}; - const testOptions = {expectRetryToSucceed: false}; - transactionTest.runInsertUpdateDeleteTests(lsid, testOptions); + const makeSessionIdFunc = () => { + return {id: UUID(), txnUUID: UUID()}; + }; + const expectRetryToSucceed = false; + transactionTest.runInsertUpdateDeleteTests( + {txnOptions: {makeSessionIdFunc}, expectRetryToSucceed}); } { diff --git a/jstests/sharding/internal_txns/retryable_writes_retry_conflict.js b/jstests/sharding/internal_txns/retryable_writes_retry_conflict.js index e2a8fbd2096..922e729b718 100644 --- a/jstests/sharding/internal_txns/retryable_writes_retry_conflict.js +++ b/jstests/sharding/internal_txns/retryable_writes_retry_conflict.js @@ -83,9 +83,14 @@ function testBlockingRetry(retryFunc, testOpts = { commitCmdObj.commitTimestamp = preparedTxnRes.prepareTimestamp; } - // Retry and wait for it to block behind the internal transaction above. - const fp = configureFailPoint( - shard0Primary, "waitAfterNewStatementBlocksBehindOpenInternalTransactionForRetryableWrite"); + let fp; + if (testOpts.prepareBeforeRetry) { + // A prepared transaction cannot be interrupted by a retry so retry and wait for it to block + // behind the internal transaction above. + fp = configureFailPoint( + shard0Primary, + "waitAfterNewStatementBlocksBehindOpenInternalTransactionForRetryableWrite"); + } const retryThread = new Thread(retryFunc, { shard0RstArgs: createRstArgs(st.rs0), parentSessionUUIDString: extractUUIDFromObject(parentLsid.id), @@ -97,18 +102,33 @@ function testBlockingRetry(retryFunc, testOpts = { stepDownPrimaryAfterBlockingRetry: testOpts.stepDownPrimaryAfterBlockingRetry }); retryThread.start(); - fp.wait(); - fp.off(); + if (testOpts.prepareBeforeRetry) { + // The retry should block behind the prepared transaction. + fp.wait(); + fp.off(); + } else { + // The retry should complete without blocking. + retryThread.join(); + } if (testOpts.stepDownPrimaryAfterBlockingRetry) { stepDownShard0Primary(); } // Commit or abort the internal transaction, and verify that the write statement executed - // exactly once despite the concurrent retry. - assert.commandWorked( - shard0TestDB.adminCommand(testOpts.abortAfterBlockingRetry ? abortCmdObj : commitCmdObj)); - retryThread.join(); + // exactly once despite the concurrent retry, whether or not the retry interrupted the original + // attempt. + if (testOpts.prepareBeforeRetry) { + assert.commandWorked(shard0TestDB.adminCommand( + testOpts.abortAfterBlockingRetry ? abortCmdObj : commitCmdObj)); + retryThread.join(); + } else { + // The retry should have interrupted the original attempt. + assert.commandFailedWithCode( + shard0TestDB.adminCommand(testOpts.abortAfterBlockingRetry ? abortCmdObj + : commitCmdObj), + ErrorCodes.NoSuchTransaction); + } assert.eq(shard0TestColl.count(docToInsert), 1); assert.commandWorked(mongosTestColl.remove({})); |