diff options
author | Cheahuychou Mao <mao.cheahuychou@gmail.com> | 2022-01-14 19:58:18 +0000 |
---|---|---|
committer | Evergreen Agent <no-reply@evergreen.mongodb.com> | 2022-01-14 21:42:59 +0000 |
commit | c20c2e8525d4877eb691b6308591590fb61740f2 (patch) | |
tree | 5ecee6c7c22df845f6a06151ed6499c40364e308 | |
parent | 86ef1c33d1fe02f2ecfae1115e03e9a1a72ef550 (diff) | |
download | mongo-c20c2e8525d4877eb691b6308591590fb61740f2.tar.gz |
SERVER-62047 Enforce txnNumber ordering in a session in the presence of retryable internal transactions
-rw-r--r-- | jstests/sharding/internal_transactions_for_retryable_writes_statement_validation.js (renamed from jstests/sharding/internal_transactions_for_retryable_writes_validation.js) | 0 | ||||
-rw-r--r-- | jstests/sharding/internal_transactions_for_retryable_writes_txn_number_validation_order.js | 351 | ||||
-rw-r--r-- | jstests/sharding/internal_transactions_for_retryable_writes_txn_number_validation_reuse.js | 280 | ||||
-rw-r--r-- | jstests/sharding/libs/sharded_transactions_helpers.js | 9 | ||||
-rw-r--r-- | src/mongo/db/transaction_participant.cpp | 56 | ||||
-rw-r--r-- | src/mongo/db/transaction_participant.h | 24 |
6 files changed, 705 insertions, 15 deletions
diff --git a/jstests/sharding/internal_transactions_for_retryable_writes_validation.js b/jstests/sharding/internal_transactions_for_retryable_writes_statement_validation.js index 7e383d412e6..7e383d412e6 100644 --- a/jstests/sharding/internal_transactions_for_retryable_writes_validation.js +++ b/jstests/sharding/internal_transactions_for_retryable_writes_statement_validation.js diff --git a/jstests/sharding/internal_transactions_for_retryable_writes_txn_number_validation_order.js b/jstests/sharding/internal_transactions_for_retryable_writes_txn_number_validation_order.js new file mode 100644 index 00000000000..4edf4b658b2 --- /dev/null +++ b/jstests/sharding/internal_transactions_for_retryable_writes_txn_number_validation_order.js @@ -0,0 +1,351 @@ +/* + * Test that transaction participants validate the txnNumber in the lsids for internal transactions + * for retryable writes against the txnNumber in the parent session and the txnNumber in the lsids + * for internal transactions for other retryable writes. In particular, test that they throw a + * TransactionTooOld error upon seeing a txnNumber lower than the highest seen one. + * + * Also test that transaction participants do not validate the txnNumber for internal transactions + * for writes without a txnNumber (i.e. non-retryable writes) against the txnNumber in + * the parent session and the txnNumber in the lsids for internal transactions for retryable writes. + * + * @tags: [requires_fcv_52, featureFlagInternalTransactions] + */ +(function() { +'use strict'; + +load('jstests/sharding/libs/sharded_transactions_helpers.js'); + +const kDbName = "testDb"; +const kCollName = "testColl"; + +const st = new ShardingTest({shards: 1}); +const shard0Primary = st.rs0.getPrimary(); + +const mongosTestDB = st.s.getDB(kDbName); +const mongosTestColl = mongosTestDB.getCollection(kCollName); +let shard0TestDB = shard0Primary.getDB(kDbName); + +assert.commandWorked(mongosTestDB.createCollection(kCollName)); + +const kTestMode = { + kNonRecovery: 1, + kRestart: 2, + kFailover: 3 +}; + +function setUpTestMode(mode) { + if (mode == kTestMode.kRestart) { + st.rs0.stopSet(null /* signal */, true /*forRestart */); + st.rs0.startSet({restart: true}); + shard0TestDB = st.rs0.getPrimary().getDB(kDbName); + } else if (mode == kTestMode.kFailover) { + const oldPrimary = st.rs0.getPrimary(); + assert.commandWorked( + oldPrimary.adminCommand({replSetStepDown: ReplSetTest.kForeverSecs, force: true})); + assert.commandWorked(oldPrimary.adminCommand({replSetFreeze: 0})); + shard0TestDB = st.rs0.getPrimary().getDB(kDbName); + } +} + +function makeInsertCmdObj(docs, {lsid, txnNumber, isTransaction}) { + const cmdObj = { + insert: kCollName, + documents: docs, + lsid: lsid, + txnNumber: NumberLong(txnNumber), + }; + if (isTransaction) { + cmdObj.startTransaction = true; + cmdObj.autocommit = false; + } + return cmdObj; +} + +// Test transaction number validation for internal transactions for writes with a txnNumber (i.e. +// retryable writes). + +function testTxnNumberValidationOnRetryingOldTxnNumber(makeOrderedSessionOptsFunc, testModeName) { + const {sessionOpts0, sessionOpts1} = makeOrderedSessionOptsFunc(); + const testMode = kTestMode[testModeName]; + + jsTest.log(`Testing txnNumber validation upon retrying ${tojson(sessionOpts0)} after running ${ + tojson(sessionOpts1)} with test mode ${testModeName}`); + + assert.commandWorked(shard0TestDB.runCommand(makeInsertCmdObj([{x: 0}], sessionOpts0))); + if (sessionOpts0.isTransaction) { + assert.commandWorked(shard0TestDB.adminCommand( + makeCommitTransactionCmdObj(sessionOpts0.lsid, sessionOpts0.txnNumber))); + } + assert.commandWorked(shard0TestDB.runCommand(makeInsertCmdObj([{x: 1}], sessionOpts1))); + if (sessionOpts1.isTransaction) { + assert.commandWorked(shard0TestDB.adminCommand( + makeCommitTransactionCmdObj(sessionOpts1.lsid, sessionOpts1.txnNumber))); + } + assert.eq(mongosTestColl.count(), 2); + + setUpTestMode(testMode); + + assert.commandFailedWithCode(shard0TestDB.runCommand(makeInsertCmdObj([{x: 0}], sessionOpts0)), + ErrorCodes.TransactionTooOld); + assert.eq(mongosTestColl.count(), 2); + + assert.commandWorked(mongosTestColl.remove({})); +} + +function testTxnNumberValidationOnStartingOldTxnNumber(makeOrderedSessionOptsFunc, testModeName) { + const orderedSessionsOpts = makeOrderedSessionOptsFunc(); + const sessionOpts0 = orderedSessionsOpts.sessionOpts1; + const sessionOpts1 = orderedSessionsOpts.sessionOpts0; + const testMode = kTestMode[testModeName]; + + jsTest.log(`Testing txnNumber validation upon starting ${tojson(sessionOpts1)} after running ${ + tojson(sessionOpts0)} with test mode ${testModeName}`); + + assert.commandWorked(shard0TestDB.runCommand(makeInsertCmdObj([{x: 0}], sessionOpts0))); + if (sessionOpts0.isTransaction && (testModeName != "kNonRecovery")) { + // Only transactions that have been prepared or committed are expected to survive failover + // and restart. + assert.commandWorked(shard0TestDB.adminCommand( + makeCommitTransactionCmdObj(sessionOpts0.lsid, sessionOpts0.txnNumber))); + } + + setUpTestMode(testMode); + + assert.commandFailedWithCode(shard0TestDB.runCommand(makeInsertCmdObj([{x: 1}], sessionOpts1)), + ErrorCodes.TransactionTooOld); + + if (sessionOpts0.isTransaction) { + assert.commandWorked(shard0TestDB.adminCommand( + makeCommitTransactionCmdObj(sessionOpts0.lsid, sessionOpts0.txnNumber))); + } + + assert.eq(mongosTestColl.count(), 1); + assert.neq(mongosTestColl.findOne({x: 0}), null); + assert.commandWorked(mongosTestColl.remove({})); +} + +function runTestsForInternalSessionForRetryableWrite(makeOrderedSessionOptsFunc) { + for (let testModeName in kTestMode) { + testTxnNumberValidationOnRetryingOldTxnNumber(makeOrderedSessionOptsFunc, testModeName); + testTxnNumberValidationOnStartingOldTxnNumber(makeOrderedSessionOptsFunc, testModeName); + } +} + +{ + let makeOrderedSessionOptsFunc = () => { + const sessionUUID = UUID(); + // Internal transaction for retryable writes in a child session. + const sessionOpts0 = { + lsid: {id: sessionUUID, txnNumber: NumberLong(5), txnUUID: UUID()}, + txnNumber: NumberLong(0), + isTransaction: true + }; + // Internal transaction for retryable writes in a child session. + const sessionOpts1 = { + lsid: {id: sessionUUID, txnNumber: NumberLong(6), txnUUID: UUID()}, + txnNumber: NumberLong(0), + isTransaction: true + }; + return {sessionOpts0, sessionOpts1}; + }; + + runTestsForInternalSessionForRetryableWrite(makeOrderedSessionOptsFunc); +} + +{ + let makeOrderedSessionOptsFunc = () => { + const sessionUUID = UUID(); + // Internal transaction for retryable writes in a child session. + const sessionOpts0 = { + lsid: {id: sessionUUID, txnNumber: NumberLong(5), txnUUID: UUID()}, + txnNumber: NumberLong(0), + isTransaction: true + }; + // Retryable writes in the parent session. + const sessionOpts1 = { + lsid: {id: sessionUUID}, + txnNumber: NumberLong(6), + isTransaction: false + }; + return {sessionOpts0, sessionOpts1}; + }; + + runTestsForInternalSessionForRetryableWrite(makeOrderedSessionOptsFunc); +} + +{ + let makeOrderedSessionOptsFunc = () => { + const sessionUUID = UUID(); + // Internal transaction for retryable writes in a child session. + const sessionOpts0 = { + lsid: {id: sessionUUID, txnNumber: NumberLong(5), txnUUID: UUID()}, + txnNumber: NumberLong(0), + isTransaction: true + }; + // Transaction in the parent session. + const sessionOpts1 = { + lsid: {id: sessionUUID}, + txnNumber: NumberLong(6), + isTransaction: true + }; + return {sessionOpts0, sessionOpts1}; + }; + + runTestsForInternalSessionForRetryableWrite(makeOrderedSessionOptsFunc); +} + +{ + let makeOrderedSessionOptsFunc = () => { + const sessionUUID = UUID(); + // Retryable writes in the parent session. + const sessionOpts0 = { + lsid: {id: sessionUUID}, + txnNumber: NumberLong(5), + isTransaction: false + }; + // Internal transaction for retryable writes in a child session. + const sessionOpts1 = { + lsid: {id: sessionUUID, txnNumber: NumberLong(6), txnUUID: UUID()}, + txnNumber: NumberLong(0), + isTransaction: true + }; + return {sessionOpts0, sessionOpts1}; + }; + + runTestsForInternalSessionForRetryableWrite(makeOrderedSessionOptsFunc); +} + +{ + let makeOrderedSessionOptsFunc = () => { + const sessionUUID = UUID(); + // Transaction in the parent session. + const sessionOpts0 = { + lsid: {id: sessionUUID}, + txnNumber: NumberLong(5), + isTransaction: true + }; + // Internal transaction for retryable writes in a child session. + const sessionOpts1 = { + lsid: {id: sessionUUID, txnNumber: NumberLong(6), txnUUID: UUID()}, + txnNumber: NumberLong(0), + isTransaction: true + }; + return {sessionOpts0, sessionOpts1}; + }; + + runTestsForInternalSessionForRetryableWrite(makeOrderedSessionOptsFunc); +} + +// Test that there is no "cross-session" transaction number validation for internal transactions for +// writes without a txnNumber (i.e. non-retryable writes). + +function runTestForInternalSessionForNonRetryableWrite(makeSessionOptsFunc) { + const {sessionOpts0, sessionOpts1} = makeSessionOptsFunc(); + + jsTest.log(`Testing that there is no txnNumber validation upon starting ${ + tojson(sessionOpts1)} after running ${tojson(sessionOpts0)}`); + + assert.commandWorked(shard0TestDB.runCommand(makeInsertCmdObj([{x: 0}], sessionOpts0))); + assert.commandWorked(shard0TestDB.runCommand(makeInsertCmdObj([{x: 1}], sessionOpts1))); + + if (sessionOpts0.isTransaction) { + assert.commandWorked(shard0TestDB.adminCommand( + makeCommitTransactionCmdObj(sessionOpts0.lsid, sessionOpts0.txnNumber))); + } + if (sessionOpts1.isTransaction) { + assert.commandWorked(shard0TestDB.adminCommand( + makeCommitTransactionCmdObj(sessionOpts1.lsid, sessionOpts1.txnNumber))); + } + + assert.eq(mongosTestColl.count(), 2); + assert.neq(mongosTestColl.findOne({x: 0}), null); + assert.neq(mongosTestColl.findOne({x: 1}), null); + assert.commandWorked(mongosTestColl.remove({})); +} + +{ + let makeSessionOptsFunc = () => { + const sessionUUID = UUID(); + // Internal transaction for non-retryable writes in a child session. + const sessionOpts0 = { + lsid: {id: sessionUUID}, + txnNumber: NumberLong(6), + isTransaction: false + }; + // Internal transaction for non-retryable writes in another child session. + const sessionOpts1 = { + lsid: {id: sessionUUID, txnUUID: UUID()}, + txnNumber: NumberLong(5), + isTransaction: true + }; + return {sessionOpts0, sessionOpts1}; + }; + + runTestForInternalSessionForNonRetryableWrite(makeSessionOptsFunc); +} + +{ + let makeSessionOptsFunc = () => { + const sessionUUID = UUID(); + // Internal transaction for non-retryable writes in a child session. + const sessionOpts0 = { + lsid: {id: sessionUUID, txnUUID: UUID()}, + txnNumber: NumberLong(6), + isTransaction: true + }; + // Retryable writes in the parent session. + const sessionOpts1 = { + lsid: {id: sessionUUID}, + txnNumber: NumberLong(5), + isTransaction: false + }; + return {sessionOpts0, sessionOpts1}; + }; + + runTestForInternalSessionForNonRetryableWrite(makeSessionOptsFunc); +} + +{ + let makeSessionOptsFunc = () => { + const sessionUUID = UUID(); + // Internal transaction for non-retryable writes in a child session. + const sessionOpts0 = { + lsid: {id: sessionUUID, txnUUID: UUID()}, + txnNumber: NumberLong(6), + isTransaction: true + }; + // Internal transaction for retryable writes in a child session. + const sessionOpts1 = { + lsid: {id: sessionUUID, txnNumber: NumberLong(5), txnUUID: UUID()}, + txnNumber: NumberLong(0), + isTransaction: true + }; + return {sessionOpts0, sessionOpts1}; + }; + + runTestForInternalSessionForNonRetryableWrite(makeSessionOptsFunc); +} + +{ + let makeSessionOptsFunc = () => { + const sessionUUID = UUID(); + // Internal transaction for retryable writes in a child session. + const sessionOpts0 = { + lsid: {id: sessionUUID, txnNumber: NumberLong(6), txnUUID: UUID()}, + txnNumber: NumberLong(0), + isTransaction: true + }; + // Internal transaction for non-retryable writes in another child session. + const sessionOpts1 = { + lsid: {id: sessionUUID, txnUUID: UUID()}, + txnNumber: NumberLong(5), + isTransaction: true + }; + return {sessionOpts0, sessionOpts1}; + }; + + runTestForInternalSessionForNonRetryableWrite(makeSessionOptsFunc); +} + +st.stop(); +})(); diff --git a/jstests/sharding/internal_transactions_for_retryable_writes_txn_number_validation_reuse.js b/jstests/sharding/internal_transactions_for_retryable_writes_txn_number_validation_reuse.js new file mode 100644 index 00000000000..5ec0e51ed41 --- /dev/null +++ b/jstests/sharding/internal_transactions_for_retryable_writes_txn_number_validation_reuse.js @@ -0,0 +1,280 @@ +/* + * Test that transaction participants validate the txnNumber in the lsids for internal transactions + * for retryable writes against the txnNumber in the parent session and the txnNumber in the lsids + * for internal transactions for other retryable writes. In particular, test that they: + * - Throw an IncompleteTransactionHistory error upon seeing a txnNumber previously used by a + * transaction being used by a retryable internal transaction. + * - Throw an error upon seeing a txnNumber previously used by a retryable internal transaction + * being used by another transaction, unless the retryable internal transaction has aborted + * without prepare. + * + * Also test that transaction participants do not validate the txnNumber for internal transactions + * for writes without a txnNumber (i.e. non-retryable writes) against the txnNumber in + * the parent session and the txnNumber in the lsids for internal transactions for retryable writes. + * + * @tags: [requires_fcv_52, featureFlagInternalTransactions] + */ +(function() { +'use strict'; + +load('jstests/sharding/libs/sharded_transactions_helpers.js'); + +const kDbName = "testDb"; +const kCollName = "testColl"; + +const st = new ShardingTest({shards: 1}); +const shard0Primary = st.rs0.getPrimary(); + +const mongosTestDB = st.s.getDB(kDbName); +const mongosTestColl = mongosTestDB.getCollection(kCollName); +let shard0TestDB = shard0Primary.getDB(kDbName); + +assert.commandWorked(mongosTestDB.createCollection(kCollName)); + +const kTestMode = { + kNonRecovery: 1, + kRestart: 2, + kFailover: 3 +}; + +const kTxnState = { + kStarted: 1, + kCommitted: 2, + kAbortedWithoutPrepare: 3, + kAbortedWithPrepare: 4 +}; + +function setUpTestMode(mode) { + if (mode == kTestMode.kRestart) { + st.rs0.stopSet(null /* signal */, true /*forRestart */); + st.rs0.startSet({restart: true}); + shard0TestDB = st.rs0.getPrimary().getDB(kDbName); + } else if (mode == kTestMode.kFailover) { + const oldPrimary = st.rs0.getPrimary(); + assert.commandWorked( + oldPrimary.adminCommand({replSetStepDown: ReplSetTest.kForeverSecs, force: true})); + assert.commandWorked(oldPrimary.adminCommand({replSetFreeze: 0})); + shard0TestDB = st.rs0.getPrimary().getDB(kDbName); + } +} + +function makeInsertCmdObj(docs, {lsid, txnNumber, isTransaction}) { + const cmdObj = { + insert: kCollName, + documents: docs, + lsid: lsid, + txnNumber: NumberLong(txnNumber), + }; + if (isTransaction) { + cmdObj.startTransaction = true; + cmdObj.autocommit = false; + } + return cmdObj; +} + +function testTxnNumberValidation( + makeSessionOptsFunc, prevTxnStateName, testModeName, expectedError) { + if ((prevTxnStateName == "kStarted" || prevTxnStateName == "kAbortedWithoutPrepare") && + (testModeName != "kNonRecovery")) { + // Invalid combination. Only transactions that have been prepared or committed are expected + // to survive failover and restart. + return; + } + + const {sessionOpts0, sessionOpts1} = makeSessionOptsFunc(); + const prevTxnState = kTxnState[prevTxnStateName]; + const testMode = kTestMode[testModeName]; + + jsTest.log(`Test txnNumber validation upon running ${tojson(sessionOpts1)} after ${ + tojson(sessionOpts0)} reaches state ${prevTxnStateName} with test mode ${testModeName}`); + + assert.commandWorked(shard0TestDB.runCommand(makeInsertCmdObj([{x: 0}], sessionOpts0))); + switch (prevTxnState) { + case kTxnState.kCommitted: + assert.commandWorked(shard0TestDB.adminCommand( + makeCommitTransactionCmdObj(sessionOpts0.lsid, sessionOpts0.txnNumber))); + break; + case kTxnState.kAbortedWithoutPrepare: + assert.commandWorked(shard0TestDB.adminCommand( + makeAbortTransactionCmdObj(sessionOpts0.lsid, sessionOpts0.txnNumber))); + break; + case kTxnState.kAbortedWithPrepare: + assert.commandWorked(shard0TestDB.adminCommand( + makePrepareTransactionCmdObj(sessionOpts0.lsid, sessionOpts0.txnNumber))); + assert.commandWorked(shard0TestDB.adminCommand( + makeAbortTransactionCmdObj(sessionOpts0.lsid, sessionOpts0.txnNumber))); + break; + default: + break; + } + + setUpTestMode(testMode); + + const reuseRes = shard0TestDB.runCommand(makeInsertCmdObj([{x: 1}], sessionOpts1)); + if (expectedError) { + assert.commandFailedWithCode(reuseRes, expectedError); + } else { + assert.commandWorked(reuseRes); + } + + assert.commandWorked(mongosTestColl.remove({})); +} + +// Test transaction number validation for internal transactions for writes with a txnNumber (i.e. +// retryable writes). + +{ + let makeSessionOptsFunc = () => { + const sessionUUID = UUID(); + // Transaction in the parent session. + const sessionOpts0 = { + lsid: {id: sessionUUID}, + txnNumber: NumberLong(5), + isTransaction: true + }; + // Internal transaction for retryable writes in a child session. + const sessionOpts1 = { + lsid: {id: sessionUUID, txnNumber: NumberLong(5), txnUUID: UUID()}, + txnNumber: NumberLong(0), + isTransaction: true + }; + return {sessionOpts0, sessionOpts1}; + }; + const expectedError = ErrorCodes.IncompleteTransactionHistory; + + for (let testModeName in kTestMode) { + for (let txnStateName in kTxnState) { + testTxnNumberValidation(makeSessionOptsFunc, txnStateName, testModeName, expectedError); + } + } +} + +{ + let makeSessionOptsFunc = () => { + const sessionUUID = UUID(); + // Internal transaction for retryable writes in a child session. + const sessionOpts0 = { + lsid: {id: sessionUUID, txnNumber: NumberLong(5), txnUUID: UUID()}, + txnNumber: NumberLong(0), + isTransaction: true + }; + // Transaction in the parent session. + const sessionOpts1 = { + lsid: {id: sessionUUID}, + txnNumber: NumberLong(5), + isTransaction: true + }; + return {sessionOpts0, sessionOpts1}; + }; + const expectedError = 6202002; + + for (let testModeName in kTestMode) { + for (let txnStateName in kTxnState) { + testTxnNumberValidation( + makeSessionOptsFunc, + txnStateName, + testModeName, + txnStateName == "kAbortedWithoutPrepare" ? null : expectedError); + } + } +} + +// Test that there is no "cross-session" transaction number validation for internal transactions for +// writes without a txnNumber (i.e. non-retryable writes). + +{ + let makeSessionOptsFunc = () => { + const sessionUUID = UUID(); + // Internal transaction for non-retryable writes in a child session. + const sessionOpts0 = { + lsid: {id: sessionUUID}, + txnNumber: NumberLong(5), + isTransaction: false + }; + // Internal transaction for non-retryable writes in another child session. + const sessionOpts1 = { + lsid: {id: sessionUUID, txnUUID: UUID()}, + txnNumber: NumberLong(5), + isTransaction: true + }; + return {sessionOpts0, sessionOpts1}; + }; + const expectedError = null; + + testTxnNumberValidation(makeSessionOptsFunc, "kStarted", "kNonRecovery", expectedError); +} + +{ + let makeSessionOptsFunc = () => { + const sessionUUID = UUID(); + // Internal transaction for non-retryable writes in a child session. + const sessionOpts0 = { + lsid: {id: sessionUUID, txnUUID: UUID()}, + txnNumber: NumberLong(5), + isTransaction: true + }; + // Retryable writes in the parent session. + const sessionOpts1 = { + lsid: {id: sessionUUID}, + txnNumber: NumberLong(5), + isTransaction: false + }; + return {sessionOpts0, sessionOpts1}; + }; + const expectedError = null; + + for (let txnStateName in kTxnState) { + testTxnNumberValidation(makeSessionOptsFunc, txnStateName, "kNonRecovery", expectedError); + } +} + +{ + let makeSessionOptsFunc = () => { + const sessionUUID = UUID(); + // Internal transaction for non-retryable writes in a child session. + const sessionOpts0 = { + lsid: {id: sessionUUID, txnUUID: UUID()}, + txnNumber: NumberLong(5), + isTransaction: true + }; + // Internal transaction for retryable writes in a child session. + const sessionOpts1 = { + lsid: {id: sessionUUID, txnNumber: NumberLong(5), txnUUID: UUID()}, + txnNumber: NumberLong(0), + isTransaction: true + }; + return {sessionOpts0, sessionOpts1}; + }; + const expectedError = null; + + for (let txnStateName in kTxnState) { + testTxnNumberValidation(makeSessionOptsFunc, txnStateName, "kNonRecovery", expectedError); + } +} + +{ + let makeSessionOptsFunc = () => { + const sessionUUID = UUID(); + // Internal transaction for retryable writes in a child session. + const sessionOpts0 = { + lsid: {id: sessionUUID, txnNumber: NumberLong(5), txnUUID: UUID()}, + txnNumber: NumberLong(0), + isTransaction: true + }; + // Internal transaction for non-retryable writes in another child session. + const sessionOpts1 = { + lsid: {id: sessionUUID, txnUUID: UUID()}, + txnNumber: NumberLong(5), + isTransaction: true + }; + return {sessionOpts0, sessionOpts1}; + }; + const expectedError = null; + + for (let txnStateName in kTxnState) { + testTxnNumberValidation(makeSessionOptsFunc, txnStateName, "kNonRecovery", expectedError); + } +} + +st.stop(); +})(); diff --git a/jstests/sharding/libs/sharded_transactions_helpers.js b/jstests/sharding/libs/sharded_transactions_helpers.js index 9e861229b73..9d3d985dee0 100644 --- a/jstests/sharding/libs/sharded_transactions_helpers.js +++ b/jstests/sharding/libs/sharded_transactions_helpers.js @@ -224,6 +224,15 @@ function getImageEntriesForTxn(rs, lsid, txnNumber) { return getImageEntriesForTxnOnNode(rs.getPrimary(), lsid, txnNumber); } +function makeAbortTransactionCmdObj(lsid, txnNumber) { + return { + abortTransaction: 1, + lsid: lsid, + txnNumber: NumberLong(txnNumber), + autocommit: false, + }; +} + function makeCommitTransactionCmdObj(lsid, txnNumber) { return { commitTransaction: 1, diff --git a/src/mongo/db/transaction_participant.cpp b/src/mongo/db/transaction_participant.cpp index 8d71149c748..1e98721e399 100644 --- a/src/mongo/db/transaction_participant.cpp +++ b/src/mongo/db/transaction_participant.cpp @@ -648,6 +648,46 @@ void TransactionParticipant::Participant::_uassertNoConflictingInternalTransacti } } +void TransactionParticipant::Participant::_uassertCanReuseActiveTxnNumberForTransaction( + OperationContext* opCtx) { + if (o().txnState.isInSet(TransactionState::kNone)) { + const auto& retryableWriteTxnParticipantCatalog = + getRetryableWriteTransactionParticipantCatalog(opCtx); + invariant(retryableWriteTxnParticipantCatalog.isValid()); + + for (const auto& it : retryableWriteTxnParticipantCatalog.getParticipants()) { + const auto& txnParticipant = it.second; + + if (txnParticipant._sessionId() == _sessionId()) { + continue; + } + + invariant(txnParticipant._isInternalSessionForRetryableWrite()); + uassert( + 6202002, + str::stream() << "Cannot start transaction with session id " << _sessionId() + << " and transaction number " + << o().activeTxnNumberAndRetryCounter.getTxnNumber() + << " because a retryable write with the same transaction number" + << " is being executed in a retryable internal transaction " + << " with session id " << txnParticipant._sessionId() + << " and transaction number " + << txnParticipant.getActiveTxnNumberAndRetryCounter().getTxnNumber() + << " in state " << txnParticipant.o().txnState, + txnParticipant.transactionIsAbortedWithoutPrepare()); + } + } else { + uassert( + 50911, + str::stream() << "Cannot start a transaction with session id " << _sessionId() + << " and transaction number " + << o().activeTxnNumberAndRetryCounter.toBSON() + << " because a transaction with the same transaction number is in state " + << o().txnState, + o().txnState.isInSet(TransactionState::kAbortedWithoutPrepare)); + } +} + void TransactionParticipant::Participant::_beginOrContinueRetryableWrite( OperationContext* opCtx, const TxnNumberAndRetryCounter& txnNumberAndRetryCounter) { invariant(!txnNumberAndRetryCounter.getTxnRetryCounter()); @@ -761,21 +801,7 @@ void TransactionParticipant::Participant::_beginMultiDocumentTransaction( return; } - // The active transaction number can only be reused if: - // 1. The transaction participant is in retryable write mode and has not yet executed a - // retryable write, or - // 2. A transaction is aborted and has not been involved in a two phase commit. - // - // Assuming routers target primaries in increasing order of term and in the absence of - // byzantine messages, this check should never fail. - const auto restartableStates = - TransactionState::kNone | TransactionState::kAbortedWithoutPrepare; - uassert(50911, - str::stream() << "Cannot start a transaction at given transaction with " - << txnNumberAndRetryCounter.toBSON() - << " because a transaction with the same number is in state " - << o().txnState, - o().txnState.isInSet(restartableStates)); + _uassertCanReuseActiveTxnNumberForTransaction(opCtx); } else { const auto restartableStates = TransactionState::kNone | TransactionState::kInProgress | TransactionState::kAbortedWithoutPrepare | TransactionState::kAbortedWithPrepare; diff --git a/src/mongo/db/transaction_participant.h b/src/mongo/db/transaction_participant.h index 0596798cf80..b3425c6001d 100644 --- a/src/mongo/db/transaction_participant.h +++ b/src/mongo/db/transaction_participant.h @@ -140,6 +140,10 @@ class TransactionParticipant { return _state == kAbortedWithPrepare || _state == kAbortedWithoutPrepare; } + bool isAbortedWithoutPrepare() const { + return _state == kAbortedWithoutPrepare; + } + bool hasExecutedRetryableWrite() const { return _state == kExecutedRetryableWrite; } @@ -312,6 +316,10 @@ public: return o().txnState.isAborted(); } + bool transactionIsAbortedWithoutPrepare() const { + return o().txnState.isAbortedWithoutPrepare(); + } + bool transactionIsPrepared() const { return o().txnState.isPrepared(); } @@ -845,6 +853,22 @@ public: void _uassertNoConflictingInternalTransactionForRetryableWrite( OperationContext* opCtx, const TxnNumberAndRetryCounter& txnNumberAndRetryCounter); + // Asserts that the active transaction number can be reused. Below are the two cases where + // an active transaction number is allowed to be reused: + // 1. The transaction participant is in transaction mode and the transaction has been + // aborted and not been involved in a two phase commit. This corresponds to the case + // where a transaction is internally retried after failing with a transient error such a + // stale config or snapshot too old or view resolution error. + // 2. The transaction participant is in retryable write mode and has not yet executed a + // retryable write. This corresponds to the case where a retryable write is converted + // to a transaction. The only use case of this is where the write fails with a + // WouldChangeOwningShard error. For a retryable write being executed using internal + // transactions, there is an additional requirement that all the internal transactions + // have been aborted and have not been involved in a two phase commit. + // Assuming routers target primaries in increasing order of term and in the absence of + // byzantine messages, this check should never fail. + void _uassertCanReuseActiveTxnNumberForTransaction(OperationContext* opCtx); + // Attempt to begin or retry a retryable write at the given transaction number. void _beginOrContinueRetryableWrite( OperationContext* opCtx, const TxnNumberAndRetryCounter& txnNumberAndRetryCounter); |