summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCheahuychou Mao <mao.cheahuychou@gmail.com>2022-01-14 19:58:18 +0000
committerEvergreen Agent <no-reply@evergreen.mongodb.com>2022-01-14 21:42:59 +0000
commitc20c2e8525d4877eb691b6308591590fb61740f2 (patch)
tree5ecee6c7c22df845f6a06151ed6499c40364e308
parent86ef1c33d1fe02f2ecfae1115e03e9a1a72ef550 (diff)
downloadmongo-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.js351
-rw-r--r--jstests/sharding/internal_transactions_for_retryable_writes_txn_number_validation_reuse.js280
-rw-r--r--jstests/sharding/libs/sharded_transactions_helpers.js9
-rw-r--r--src/mongo/db/transaction_participant.cpp56
-rw-r--r--src/mongo/db/transaction_participant.h24
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);