summaryrefslogtreecommitdiff
path: root/jstests/sharding/internal_txns
diff options
context:
space:
mode:
Diffstat (limited to 'jstests/sharding/internal_txns')
-rw-r--r--jstests/sharding/internal_txns/libs/chunk_migration_test.js68
-rw-r--r--jstests/sharding/internal_txns/libs/fixture_helpers.js37
-rw-r--r--jstests/sharding/internal_txns/libs/resharding_test.js100
-rw-r--r--jstests/sharding/internal_txns/libs/retryable_internal_transaction_test.js413
-rw-r--r--jstests/sharding/internal_txns/overwrite_txns.js292
-rw-r--r--jstests/sharding/internal_txns/partial_index.js525
-rw-r--r--jstests/sharding/internal_txns/retryable_findAndModify_basic.js18
-rw-r--r--jstests/sharding/internal_txns/retryable_writes_basic.js18
-rw-r--r--jstests/sharding/internal_txns/retryable_writes_retry_conflict.js38
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({}));