summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--buildscripts/resmokeconfig/suites/causally_consistent_jscore_txns_passthrough.yml3
-rw-r--r--buildscripts/resmokeconfig/suites/sharded_core_txns.yml1
-rw-r--r--jstests/core/txns/prepare_conflict.js17
-rw-r--r--jstests/core/txns/prepare_conflict_read_concern_behavior.js134
-rw-r--r--jstests/core/txns/timestamped_reads_wait_for_prepare_oplog_visibility.js12
-rw-r--r--src/mongo/db/read_concern.cpp7
-rw-r--r--src/mongo/db/storage/wiredtiger/wiredtiger_recovery_unit.cpp3
-rw-r--r--src/mongo/db/storage/wiredtiger/wiredtiger_snapshot_manager.cpp4
-rw-r--r--src/mongo/db/storage/wiredtiger/wiredtiger_snapshot_manager.h3
9 files changed, 168 insertions, 16 deletions
diff --git a/buildscripts/resmokeconfig/suites/causally_consistent_jscore_txns_passthrough.yml b/buildscripts/resmokeconfig/suites/causally_consistent_jscore_txns_passthrough.yml
index 113822c6afd..a9fc9b89429 100644
--- a/buildscripts/resmokeconfig/suites/causally_consistent_jscore_txns_passthrough.yml
+++ b/buildscripts/resmokeconfig/suites/causally_consistent_jscore_txns_passthrough.yml
@@ -10,6 +10,9 @@ selector:
exclude_files:
# The following tests are excluded because they do not use the transactions shell helpers.
- jstests/core/txns/non_transactional_operations_on_session_with_transaction.js
+ # These tests rely on having read concerns that don't use afterClusterTime.
+ - jstests/core/txns/prepare_conflict_read_concern_behavior.js
+ - jstests/core/txns/timestamped_reads_wait_for_prepare_oplog_visibility.js
executor:
archive:
diff --git a/buildscripts/resmokeconfig/suites/sharded_core_txns.yml b/buildscripts/resmokeconfig/suites/sharded_core_txns.yml
index 6eb39091cd6..c3c35928504 100644
--- a/buildscripts/resmokeconfig/suites/sharded_core_txns.yml
+++ b/buildscripts/resmokeconfig/suites/sharded_core_txns.yml
@@ -23,6 +23,7 @@ selector:
- jstests/core/txns/no_new_transactions_when_prepared_transaction_in_progress.js
- jstests/core/txns/prepare_committed_transaction.js
- jstests/core/txns/prepare_conflict.js
+ - jstests/core/txns/prepare_conflict_read_concern_behavior.js
- jstests/core/txns/prepare_nonexistent_transaction.js
- jstests/core/txns/prepare_prepared_transaction.js
- jstests/core/txns/prepare_requires_fcv42.js
diff --git a/jstests/core/txns/prepare_conflict.js b/jstests/core/txns/prepare_conflict.js
index 870af78909a..a06ddd296f0 100644
--- a/jstests/core/txns/prepare_conflict.js
+++ b/jstests/core/txns/prepare_conflict.js
@@ -5,6 +5,7 @@
*/
(function() {
"use strict";
+ load("jstests/core/txns/libs/prepare_helpers.js");
const dbName = "test";
const collName = "prepare_conflict";
@@ -14,9 +15,15 @@
testColl.drop({writeConcern: {w: "majority"}});
assert.commandWorked(testDB.runCommand({create: collName, writeConcern: {w: "majority"}}));
- function assertPrepareConflict(filter) {
+ function assertPrepareConflict(filter, clusterTime) {
assert.commandFailedWithCode(
- testDB.runCommand({find: collName, filter: filter, maxTimeMS: 1000}),
+ // Use afterClusterTime read to make sure that it will block on a prepare conflict.
+ testDB.runCommand({
+ find: collName,
+ filter: filter,
+ readConcern: {afterClusterTime: clusterTime},
+ maxTimeMS: 1000
+ }),
ErrorCodes.MaxTimeMSExpired);
let prepareConflicted = false;
@@ -55,13 +62,13 @@
updates: [{q: txnDoc, u: {$inc: {x: 1}}}],
}));
- assert.commandWorked(sessionDB.adminCommand({prepareTransaction: 1}));
+ const prepareTimestamp = PrepareHelpers.prepareTransaction(session);
// Conflict on _id of prepared document.
- assertPrepareConflict({_id: txnDoc._id});
+ assertPrepareConflict({_id: txnDoc._id}, prepareTimestamp);
// Conflict on field that could be added to a prepared document.
- assertPrepareConflict({randomField: "random"});
+ assertPrepareConflict({randomField: "random"}, prepareTimestamp);
// No conflict on _id of a non-prepared document.
assert.commandWorked(testDB.runCommand({find: collName, filter: {_id: otherDoc._id}}));
diff --git a/jstests/core/txns/prepare_conflict_read_concern_behavior.js b/jstests/core/txns/prepare_conflict_read_concern_behavior.js
new file mode 100644
index 00000000000..6b9677a38a9
--- /dev/null
+++ b/jstests/core/txns/prepare_conflict_read_concern_behavior.js
@@ -0,0 +1,134 @@
+/**
+ * Test calling reads with various read concerns on a prepared transaction. Snapshot, linearizable
+ * and afterClusterTime reads are the only reads that should block on a prepared transaction.
+ *
+ * @tags: [uses_transactions]
+ */
+
+(function() {
+ "use strict";
+ load("jstests/core/txns/libs/prepare_helpers.js");
+
+ const failureTimeout = 1 * 1000; // 1 second.
+ const successTimeout = 5 * 60 * 1000; // 5 minutes.
+ const dbName = "test";
+ const collName = "prepare_conflict_read_concern_behavior";
+ const collName2 = "prepare_conflict_read_concern_behavior2";
+ const testDB = db.getSiblingDB(dbName);
+ const testColl = testDB.getCollection(collName);
+ const testColl2 = testDB.getCollection(collName2);
+
+ testDB.runCommand({drop: collName, writeConcern: {w: "majority"}});
+ assert.commandWorked(testDB.runCommand({create: collName, writeConcern: {w: "majority"}}));
+
+ const session = db.getMongo().startSession({causalConsistency: false});
+ const sessionDB = session.getDatabase(dbName);
+ const sessionColl = sessionDB.getCollection(collName);
+
+ const read = function(read_concern, timeout, db, coll, num_expected) {
+ let res = db.runCommand({
+ find: coll,
+ filter: {in_prepared_txn: 3},
+ readConcern: read_concern,
+ maxTimeMS: timeout,
+ });
+
+ if (num_expected) {
+ assert.eq(res.cursor.firstBatch.length, num_expected, tojson(res));
+ }
+ return res;
+ };
+
+ const clusterTimeBeforePrepare =
+ assert
+ .commandWorked(testColl.runCommand(
+ "insert",
+ {documents: [{_id: 1, in_prepared_txn: 3}], writeConcern: {w: "majority"}}))
+ .operationTime;
+ testColl2.runCommand("insert", {documents: [{_id: 1, in_prepared_txn: 3}]});
+
+ session.startTransaction();
+ assert.commandWorked(sessionColl.insert({_id: 2}));
+ const prepareTimestamp = PrepareHelpers.prepareTransaction(session);
+
+ const clusterTimeAfterPrepare =
+ assert
+ .commandWorked(testColl.runCommand(
+ "insert",
+ {documents: [{_id: 3, in_prepared_txn: 3}], writeConcern: {w: "majority"}}))
+ .operationTime;
+
+ assert.gt(prepareTimestamp, clusterTimeBeforePrepare);
+ assert.gt(clusterTimeAfterPrepare, prepareTimestamp);
+
+ jsTestLog("Test read with read concern 'majority' doesn't block on a prepared transaction.");
+ assert.commandWorked(read({level: 'majority'}, successTimeout, testDB, collName, 2));
+
+ jsTestLog("Test read with read concern 'local' doesn't block on a prepared transaction.");
+ assert.commandWorked(read({level: 'local'}, successTimeout, testDB, collName, 2));
+
+ jsTestLog("Test read with read concern 'available' doesn't block on a prepared transaction.");
+ assert.commandWorked(read({level: 'available'}, successTimeout, testDB, collName, 2));
+
+ jsTestLog("Test read with read concern 'linearizable' blocks on a prepared transaction.");
+ assert.commandFailedWithCode(read({level: 'linearizable'}, failureTimeout, testDB, collName),
+ ErrorCodes.MaxTimeMSExpired);
+
+ // TODO SERVER-36953: uncomment this test
+ // jsTestLog("Test afterClusterTime read before prepareTimestamp doesn't block on a prepared " +
+ // "transaction.");
+ // assert.commandWorked(read({level: 'local', afterClusterTime: clusterTimeBeforePrepare},
+ // successTimeout,
+ // testDB,
+ // collName,
+ // 2));
+
+ jsTestLog("Test afterClusterTime read after prepareTimestamp blocks on a prepared " +
+ "transaction.");
+ assert.commandFailedWithCode(read({level: 'local', afterClusterTime: clusterTimeAfterPrepare},
+ failureTimeout,
+ testDB,
+ collName),
+ ErrorCodes.MaxTimeMSExpired);
+
+ jsTestLog("Test read with afterClusterTime after prepareTimestamp on non-prepared documents " +
+ "doesn't block on a prepared transaction.");
+ assert.commandWorked(read({level: 'local', afterClusterTime: clusterTimeAfterPrepare},
+ successTimeout,
+ testDB,
+ collName2,
+ 1));
+
+ // Create a second session and start a new transaction to test snapshot reads.
+ const session2 = db.getMongo().startSession({causalConsistency: false});
+ const sessionDB2 = session2.getDatabase(dbName);
+ const sessionColl2 = sessionDB2.getCollection(collName);
+ // This makes future reads in the transaction use a read timestamp after the prepareTimestamp.
+ session2.startTransaction(
+ {readConcern: {level: "snapshot", atClusterTime: clusterTimeAfterPrepare}});
+
+ jsTestLog("Test read with read concern 'snapshot' and a read timestamp after prepareTimestamp" +
+ " on non-prepared documents doesn't block on a prepared transaction.");
+ assert.commandWorked(read({}, failureTimeout, sessionDB2, collName2, 1));
+
+ jsTestLog("Test read with read concern 'snapshot' and a read timestamp after prepareTimestamp" +
+ " blocks on a prepared transaction.");
+ assert.commandFailedWithCode(read({}, failureTimeout, sessionDB2, collName),
+ ErrorCodes.MaxTimeMSExpired);
+
+ session2.abortTransaction();
+ session2.startTransaction(
+ {readConcern: {level: "snapshot", atClusterTime: clusterTimeBeforePrepare}});
+
+ jsTestLog("Test read with read concern 'snapshot' and atClusterTime before " +
+ "prepareTimestamp doesn't block on a prepared transaction.");
+ assert.commandWorked(
+ testColl.runCommand("insert", {documents: [{_id: 4, in_prepared_txn: 3}]}));
+ assert.commandWorked(read({}, successTimeout, sessionDB2, collName, 1));
+
+ session.abortTransaction();
+ session.endSession();
+
+ session2.abortTransaction();
+ session2.endSession();
+}()); \ No newline at end of file
diff --git a/jstests/core/txns/timestamped_reads_wait_for_prepare_oplog_visibility.js b/jstests/core/txns/timestamped_reads_wait_for_prepare_oplog_visibility.js
index 74671ee8439..7899a1c7a14 100644
--- a/jstests/core/txns/timestamped_reads_wait_for_prepare_oplog_visibility.js
+++ b/jstests/core/txns/timestamped_reads_wait_for_prepare_oplog_visibility.js
@@ -207,14 +207,14 @@
const prepareConflict = function() {
jsTestLog("Ordinary reads should not block on prepared transactions.");
- // TODO (SERVER-36382): Uncomment this block when local reads don't cause prepare
- // conflicts.
- // cursor = assert.commandWorked(_db.runCommand(
- // {find: _collName, filter: TestData.txnDocFilter, maxTimeMS:
- // TestData.successTimeout}));
- // assert.docEq(cursor.cursor.firstBatch, [TestData.txnDoc], tojson(cursor));
let cursor = assert.commandWorked(_db.runCommand({
find: _collName,
+ filter: TestData.txnDocFilter,
+ maxTimeMS: TestData.successTimeout
+ }));
+ assert.docEq(cursor.cursor.firstBatch, [TestData.txnDoc], tojson(cursor));
+ cursor = assert.commandWorked(_db.runCommand({
+ find: _collName,
filter: TestData.otherDocFilter,
maxTimeMS: TestData.successTimeout
}));
diff --git a/src/mongo/db/read_concern.cpp b/src/mongo/db/read_concern.cpp
index cb8df144d25..53372e23878 100644
--- a/src/mongo/db/read_concern.cpp
+++ b/src/mongo/db/read_concern.cpp
@@ -334,7 +334,12 @@ Status waitForReadConcern(OperationContext* opCtx,
LOG(debugLevel) << "Using 'committed' snapshot: " << CurOp::get(opCtx)->opDescription();
}
- if (readConcernArgs.getLevel() == repl::ReadConcernLevel::kAvailableReadConcern) {
+ // Only snapshot, linearizable and afterClusterTime reads should block on prepared transactions.
+ // We don't ignore prepare conflicts if we are in a direct client in case this overrides
+ // behavior set by a higher-level operation.
+ if (readConcernArgs.getLevel() != repl::ReadConcernLevel::kSnapshotReadConcern &&
+ readConcernArgs.getLevel() != repl::ReadConcernLevel::kLinearizableReadConcern &&
+ !afterClusterTime && !atClusterTime && !opCtx->getClient()->isInDirectClient()) {
opCtx->recoveryUnit()->setIgnorePrepared(true);
}
diff --git a/src/mongo/db/storage/wiredtiger/wiredtiger_recovery_unit.cpp b/src/mongo/db/storage/wiredtiger/wiredtiger_recovery_unit.cpp
index d73a1e1faf0..1cc671dcf56 100644
--- a/src/mongo/db/storage/wiredtiger/wiredtiger_recovery_unit.cpp
+++ b/src/mongo/db/storage/wiredtiger/wiredtiger_recovery_unit.cpp
@@ -358,7 +358,8 @@ void WiredTigerRecoveryUnit::_txnOpen() {
// We reset _majorityCommittedSnapshot to the actual read timestamp used when the
// transaction was started.
_majorityCommittedSnapshot =
- _sessionCache->snapshotManager().beginTransactionOnCommittedSnapshot(session);
+ _sessionCache->snapshotManager().beginTransactionOnCommittedSnapshot(
+ session, _ignorePrepared);
break;
}
case ReadSource::kLastApplied: {
diff --git a/src/mongo/db/storage/wiredtiger/wiredtiger_snapshot_manager.cpp b/src/mongo/db/storage/wiredtiger/wiredtiger_snapshot_manager.cpp
index 77b6d875a59..011db3e3566 100644
--- a/src/mongo/db/storage/wiredtiger/wiredtiger_snapshot_manager.cpp
+++ b/src/mongo/db/storage/wiredtiger/wiredtiger_snapshot_manager.cpp
@@ -68,8 +68,8 @@ boost::optional<Timestamp> WiredTigerSnapshotManager::getMinSnapshotForNextCommi
}
Timestamp WiredTigerSnapshotManager::beginTransactionOnCommittedSnapshot(
- WT_SESSION* session) const {
- WiredTigerBeginTxnBlock txnOpen(session);
+ WT_SESSION* session, WiredTigerBeginTxnBlock::IgnorePrepared ignorePrepared) const {
+ WiredTigerBeginTxnBlock txnOpen(session, ignorePrepared);
stdx::lock_guard<stdx::mutex> lock(_committedSnapshotMutex);
uassert(ErrorCodes::ReadConcernMajorityNotAvailableYet,
diff --git a/src/mongo/db/storage/wiredtiger/wiredtiger_snapshot_manager.h b/src/mongo/db/storage/wiredtiger/wiredtiger_snapshot_manager.h
index 493e8dfb1ee..b0592cf9986 100644
--- a/src/mongo/db/storage/wiredtiger/wiredtiger_snapshot_manager.h
+++ b/src/mongo/db/storage/wiredtiger/wiredtiger_snapshot_manager.h
@@ -62,7 +62,8 @@ public:
*
* Throws if there is currently no committed snapshot.
*/
- Timestamp beginTransactionOnCommittedSnapshot(WT_SESSION* session) const;
+ Timestamp beginTransactionOnCommittedSnapshot(
+ WT_SESSION* session, WiredTigerBeginTxnBlock::IgnorePrepared ignorePrepared) const;
/**
* Starts a transaction on the last stable local timestamp, set by setLocalSnapshot.