diff options
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. |