summaryrefslogtreecommitdiff
path: root/src/mongo/db
diff options
context:
space:
mode:
authorRandolph Tan <randolph@10gen.com>2018-06-29 10:19:48 -0400
committerRandolph Tan <randolph@10gen.com>2018-08-08 16:00:20 -0400
commit52b2cc0886cdb992e2491067bdc029301d5bb6af (patch)
treec414da256a84890af3fb084ab699b572b49dab4f /src/mongo/db
parent210bb5d91cb3c77bb3ed169114f8b85cd1062fb3 (diff)
downloadmongo-52b2cc0886cdb992e2491067bdc029301d5bb6af.tar.gz
SERVER-35900 Refactor transaction machinery out from Session class
Diffstat (limited to 'src/mongo/db')
-rw-r--r--src/mongo/db/SConscript14
-rw-r--r--src/mongo/db/catalog/index_catalog_entry_impl.cpp24
-rw-r--r--src/mongo/db/commands/dbhash.cpp10
-rw-r--r--src/mongo/db/commands/find_and_modify.cpp7
-rw-r--r--src/mongo/db/commands/find_cmd.cpp8
-rw-r--r--src/mongo/db/commands/run_aggregate.cpp11
-rw-r--r--src/mongo/db/commands/txn_cmds.cpp40
-rw-r--r--src/mongo/db/commands/write_commands/write_commands.cpp6
-rw-r--r--src/mongo/db/db_raii.cpp6
-rw-r--r--src/mongo/db/kill_sessions_local.cpp20
-rw-r--r--src/mongo/db/op_observer_impl.cpp53
-rw-r--r--src/mongo/db/op_observer_impl_test.cpp90
-rw-r--r--src/mongo/db/operation_context_session_mongod.cpp53
-rw-r--r--src/mongo/db/operation_context_session_mongod.h55
-rw-r--r--src/mongo/db/ops/write_ops_exec.cpp59
-rw-r--r--src/mongo/db/periodic_runner_job_abort_expired_transactions.cpp2
-rw-r--r--src/mongo/db/pipeline/mongod_process_interface.cpp23
-rw-r--r--src/mongo/db/pipeline/pipeline_d.cpp1
-rw-r--r--src/mongo/db/read_concern.cpp14
-rw-r--r--src/mongo/db/repl/do_txn.cpp11
-rw-r--r--src/mongo/db/repl/do_txn_test.cpp15
-rw-r--r--src/mongo/db/repl/replication_coordinator_impl.cpp12
-rw-r--r--src/mongo/db/s/coordinate_commit_transaction_command.cpp14
-rw-r--r--src/mongo/db/s/session_catalog_migration_destination.cpp5
-rw-r--r--src/mongo/db/s/session_catalog_migration_destination_test.cpp7
-rw-r--r--src/mongo/db/service_entry_point_common.cpp44
-rw-r--r--src/mongo/db/session.cpp1100
-rw-r--r--src/mongo/db/session.h494
-rw-r--r--src/mongo/db/session_catalog.cpp19
-rw-r--r--src/mongo/db/session_catalog.h7
-rw-r--r--src/mongo/db/session_catalog_test.cpp95
-rw-r--r--src/mongo/db/session_test.cpp2633
-rw-r--r--src/mongo/db/transaction_participant.cpp1149
-rw-r--r--src/mongo/db/transaction_participant.h490
-rw-r--r--src/mongo/db/transaction_participant_test.cpp2438
35 files changed, 4536 insertions, 4493 deletions
diff --git a/src/mongo/db/SConscript b/src/mongo/db/SConscript
index 2618188f896..75360bf6fff 100644
--- a/src/mongo/db/SConscript
+++ b/src/mongo/db/SConscript
@@ -620,6 +620,7 @@ env.Library(
target='catalog_raii',
source=[
'catalog_raii.cpp',
+ 'operation_context_session_mongod.cpp',
'retryable_writes_stats.cpp',
'server_transactions_metrics.cpp',
'session.cpp',
@@ -1908,6 +1909,19 @@ env.CppUnitTest(
)
env.CppUnitTest(
+ target='transaction_participant_test',
+ source=[
+ 'transaction_participant_test.cpp',
+ ],
+ LIBDEPS=[
+ '$BUILD_DIR/mongo/client/read_preference',
+ '$BUILD_DIR/mongo/db/auth/authmocks',
+ '$BUILD_DIR/mongo/db/repl/mock_repl_coord_server_fixture',
+ 'query_exec',
+ ],
+)
+
+env.CppUnitTest(
target='transaction_history_iterator_test',
source=[
'transaction_history_iterator_test.cpp',
diff --git a/src/mongo/db/catalog/index_catalog_entry_impl.cpp b/src/mongo/db/catalog/index_catalog_entry_impl.cpp
index 09859e35b4c..6c38985911d 100644
--- a/src/mongo/db/catalog/index_catalog_entry_impl.cpp
+++ b/src/mongo/db/catalog/index_catalog_entry_impl.cpp
@@ -48,7 +48,7 @@
#include "mongo/db/operation_context.h"
#include "mongo/db/query/collation/collator_factory_interface.h"
#include "mongo/db/service_context.h"
-#include "mongo/db/session_catalog.h"
+#include "mongo/db/transaction_participant.h"
#include "mongo/stdx/memory.h"
#include "mongo/util/log.h"
#include "mongo/util/scopeguard.h"
@@ -162,12 +162,12 @@ const RecordId& IndexCatalogEntryImpl::head(OperationContext* opCtx) const {
}
bool IndexCatalogEntryImpl::isReady(OperationContext* opCtx) const {
- auto session = OperationContextSession::get(opCtx);
+ auto txnParticipant = TransactionParticipant::get(opCtx);
// For multi-document transactions, we can open a snapshot prior to checking the
// minimumSnapshotVersion on a collection. This means we are unprotected from reading
// out-of-sync index catalog entries. To fix this, we uassert if we detect that the
// in-memory catalog is out-of-sync with the on-disk catalog.
- if (session && session->inMultiDocumentTransaction()) {
+ if (txnParticipant && txnParticipant->inMultiDocumentTransaction()) {
if (!_catalogIsPresent(opCtx) || _catalogIsReady(opCtx) != _isReady) {
uasserted(ErrorCodes::SnapshotUnavailable,
str::stream() << "Unable to read from a snapshot due to pending collection"
@@ -192,12 +192,12 @@ bool IndexCatalogEntryImpl::isMultikey(OperationContext* opCtx) const {
// To accomplish this, the write-path will persist multikey changes on the `Session` object
// and the read-path will query this state before determining there is no interesting multikey
// state. Note, it's always legal, though potentially wasteful, to return `true`.
- auto session = OperationContextSession::get(opCtx);
- if (!session || !session->inMultiDocumentTransaction()) {
+ auto txnParticipant = TransactionParticipant::get(opCtx);
+ if (!txnParticipant || !txnParticipant->inMultiDocumentTransaction()) {
return false;
}
- for (const MultikeyPathInfo& path : session->getMultikeyPathInfo()) {
+ for (const MultikeyPathInfo& path : txnParticipant->getMultikeyPathInfo()) {
if (path.nss == NamespaceString(_ns) && path.indexName == _descriptor->indexName()) {
return true;
}
@@ -209,13 +209,13 @@ bool IndexCatalogEntryImpl::isMultikey(OperationContext* opCtx) const {
MultikeyPaths IndexCatalogEntryImpl::getMultikeyPaths(OperationContext* opCtx) const {
stdx::lock_guard<stdx::mutex> lk(_indexMultikeyPathsMutex);
- auto session = OperationContextSession::get(opCtx);
- if (!session || !session->inMultiDocumentTransaction()) {
+ auto txnParticipant = TransactionParticipant::get(opCtx);
+ if (!txnParticipant || !txnParticipant->inMultiDocumentTransaction()) {
return _indexMultikeyPaths;
}
MultikeyPaths ret = _indexMultikeyPaths;
- for (const MultikeyPathInfo& path : session->getMultikeyPathInfo()) {
+ for (const MultikeyPathInfo& path : txnParticipant->getMultikeyPathInfo()) {
if (path.nss == NamespaceString(_ns) && path.indexName == _descriptor->indexName()) {
MultikeyPathTracker::mergeMultikeyPaths(&ret, path.multikeyPaths);
}
@@ -342,13 +342,13 @@ void IndexCatalogEntryImpl::setMultikey(OperationContext* opCtx,
});
// Keep multikey changes in memory to correctly service later reads using this index.
- auto session = OperationContextSession::get(opCtx);
- if (session && session->inMultiDocumentTransaction()) {
+ auto txnParticipant = TransactionParticipant::get(opCtx);
+ if (txnParticipant && txnParticipant->inMultiDocumentTransaction()) {
MultikeyPathInfo info;
info.nss = _collection->ns();
info.indexName = _descriptor->indexName();
info.multikeyPaths = paths;
- session->addMultikeyPathInfo(std::move(info));
+ txnParticipant->addMultikeyPathInfo(std::move(info));
}
}
diff --git a/src/mongo/db/commands/dbhash.cpp b/src/mongo/db/commands/dbhash.cpp
index 61a9333c52d..2c2139cca4a 100644
--- a/src/mongo/db/commands/dbhash.cpp
+++ b/src/mongo/db/commands/dbhash.cpp
@@ -45,7 +45,7 @@
#include "mongo/db/exec/working_set_common.h"
#include "mongo/db/namespace_string.h"
#include "mongo/db/query/internal_plans.h"
-#include "mongo/db/session_catalog.h"
+#include "mongo/db/transaction_participant.h"
#include "mongo/stdx/mutex.h"
#include "mongo/util/log.h"
#include "mongo/util/md5.hpp"
@@ -115,8 +115,8 @@ public:
// We lock the entire database in S-mode in order to ensure that the contents will not
// change for the snapshot.
auto lockMode = LockMode::MODE_S;
- auto* session = OperationContextSession::get(opCtx);
- if (session && session->inMultiDocumentTransaction()) {
+ auto txnParticipant = TransactionParticipant::get(opCtx);
+ if (txnParticipant && txnParticipant->inMultiDocumentTransaction()) {
// However, if we are inside a multi-statement transaction, then we only need to lock
// the database in intent mode to ensure that none of the collections get dropped.
lockMode = getLockModeForQuery(opCtx);
@@ -218,8 +218,8 @@ private:
return "";
boost::optional<Lock::CollectionLock> collLock;
- auto* session = OperationContextSession::get(opCtx);
- if (session && session->inMultiDocumentTransaction()) {
+ auto txnParticipant = TransactionParticipant::get(opCtx);
+ if (txnParticipant && txnParticipant->inMultiDocumentTransaction()) {
// When inside a multi-statement transaction, we are only holding the database lock in
// intent mode. We need to also acquire the collection lock in intent mode to ensure
// reading from the consistent snapshot doesn't overlap with any catalog operations on
diff --git a/src/mongo/db/commands/find_and_modify.cpp b/src/mongo/db/commands/find_and_modify.cpp
index 070557d4f41..b56ad230b4d 100644
--- a/src/mongo/db/commands/find_and_modify.cpp
+++ b/src/mongo/db/commands/find_and_modify.cpp
@@ -66,6 +66,7 @@
#include "mongo/db/s/collection_sharding_state.h"
#include "mongo/db/session_catalog.h"
#include "mongo/db/stats/top.h"
+#include "mongo/db/transaction_participant.h"
#include "mongo/db/write_concern.h"
#include "mongo/util/log.h"
#include "mongo/util/scopeguard.h"
@@ -335,8 +336,8 @@ public:
if (shouldBypassDocumentValidationForCommand(cmdObj))
maybeDisableValidation.emplace(opCtx);
- const auto session = OperationContextSession::get(opCtx);
- const auto inTransaction = session && session->inMultiDocumentTransaction();
+ const auto txnParticipant = TransactionParticipant::get(opCtx);
+ const auto inTransaction = txnParticipant && txnParticipant->inMultiDocumentTransaction();
uassert(50781,
str::stream() << "Cannot write to system collection " << nsString.ns()
<< " within a transaction.",
@@ -350,7 +351,7 @@ public:
const auto stmtId = 0;
- if (opCtx->getTxnNumber()) {
+ if (opCtx->getTxnNumber() && !inTransaction) {
auto session = OperationContextSession::get(opCtx);
if (auto entry =
session->checkStatementExecuted(opCtx, *opCtx->getTxnNumber(), stmtId)) {
diff --git a/src/mongo/db/commands/find_cmd.cpp b/src/mongo/db/commands/find_cmd.cpp
index 7c82c31da29..495395e3bc6 100644
--- a/src/mongo/db/commands/find_cmd.cpp
+++ b/src/mongo/db/commands/find_cmd.cpp
@@ -48,8 +48,8 @@
#include "mongo/db/s/collection_sharding_state.h"
#include "mongo/db/server_parameters.h"
#include "mongo/db/service_context.h"
-#include "mongo/db/session_catalog.h"
#include "mongo/db/stats/counters.h"
+#include "mongo/db/transaction_participant.h"
#include "mongo/rpc/get_status_from_command_result.h"
#include "mongo/util/log.h"
@@ -228,11 +228,11 @@ public:
isExplain));
auto replCoord = repl::ReplicationCoordinator::get(opCtx);
- const auto session = OperationContextSession::get(opCtx);
+ const auto txnParticipant = TransactionParticipant::get(opCtx);
uassert(ErrorCodes::InvalidOptions,
"It is illegal to open a tailable cursor in a transaction",
- session == nullptr ||
- !(session->inMultiDocumentTransaction() && qr->isTailable()));
+ !txnParticipant ||
+ !(txnParticipant->inMultiDocumentTransaction() && qr->isTailable()));
// Validate term before acquiring locks, if provided.
if (auto term = qr->getReplicationTerm()) {
diff --git a/src/mongo/db/commands/run_aggregate.cpp b/src/mongo/db/commands/run_aggregate.cpp
index 358028ac6b0..fda10f556ab 100644
--- a/src/mongo/db/commands/run_aggregate.cpp
+++ b/src/mongo/db/commands/run_aggregate.cpp
@@ -62,8 +62,8 @@
#include "mongo/db/repl/read_concern_args.h"
#include "mongo/db/s/sharding_state.h"
#include "mongo/db/service_context.h"
-#include "mongo/db/session_catalog.h"
#include "mongo/db/storage/storage_options.h"
+#include "mongo/db/transaction_participant.h"
#include "mongo/db/views/view.h"
#include "mongo/db/views/view_catalog.h"
#include "mongo/stdx/memory.h"
@@ -371,10 +371,10 @@ Status runAggregate(OperationContext* opCtx,
// Check whether the parsed pipeline supports the given read concern.
liteParsedPipeline.assertSupportsReadConcern(opCtx, request.getExplain());
} catch (const DBException& ex) {
- auto session = OperationContextSession::get(opCtx);
+ auto txnParticipant = TransactionParticipant::get(opCtx);
// If we are in a multi-document transaction, we intercept the 'readConcern'
// assertion in order to provide a more descriptive error message and code.
- if (session && session->inMultiDocumentTransaction()) {
+ if (txnParticipant && txnParticipant->inMultiDocumentTransaction()) {
return {ErrorCodes::OperationNotSupportedInTransaction,
ex.toStatus("Operation not permitted in transaction").reason()};
}
@@ -495,8 +495,9 @@ Status runAggregate(OperationContext* opCtx,
uassertStatusOK(resolveInvolvedNamespaces(opCtx, request)),
uuid));
expCtx->tempDir = storageGlobalParams.dbpath + "/_tmp";
- auto session = OperationContextSession::get(opCtx);
- expCtx->inMultiDocumentTransaction = session && session->inMultiDocumentTransaction();
+ auto txnParticipant = TransactionParticipant::get(opCtx);
+ expCtx->inMultiDocumentTransaction =
+ txnParticipant && txnParticipant->inMultiDocumentTransaction();
auto pipeline = uassertStatusOK(Pipeline::parse(request.getPipeline(), expCtx));
diff --git a/src/mongo/db/commands/txn_cmds.cpp b/src/mongo/db/commands/txn_cmds.cpp
index a7914305961..a2cd9ba8d21 100644
--- a/src/mongo/db/commands/txn_cmds.cpp
+++ b/src/mongo/db/commands/txn_cmds.cpp
@@ -39,7 +39,7 @@
#include "mongo/db/operation_context.h"
#include "mongo/db/repl/repl_client_info.h"
#include "mongo/db/service_context.h"
-#include "mongo/db/session_catalog.h"
+#include "mongo/db/transaction_participant.h"
namespace mongo {
namespace {
@@ -77,12 +77,13 @@ public:
IDLParserErrorContext ctx("commitTransaction");
auto cmd = CommitTransaction::parse(ctx, cmdObj);
- auto session = OperationContextSession::get(opCtx);
- uassert(
- ErrorCodes::CommandFailed, "commitTransaction must be run within a session", session);
+ auto txnParticipant = TransactionParticipant::get(opCtx);
+ uassert(ErrorCodes::CommandFailed,
+ "commitTransaction must be run within a transaction",
+ txnParticipant);
// commitTransaction is retryable.
- if (session->transactionIsCommitted()) {
+ if (txnParticipant->transactionIsCommitted()) {
// We set the client last op to the last optime observed by the system to ensure that
// we wait for the specified write concern on an optime greater than or equal to the
// commit oplog entry.
@@ -93,15 +94,15 @@ public:
uassert(ErrorCodes::NoSuchTransaction,
"Transaction isn't in progress",
- session->inMultiDocumentTransaction());
+ txnParticipant->inMultiDocumentTransaction());
auto optionalCommitTimestamp = cmd.getCommitTimestamp();
if (optionalCommitTimestamp) {
// commitPreparedTransaction will throw if the transaction is not prepared.
- session->commitPreparedTransaction(opCtx, optionalCommitTimestamp.get());
+ txnParticipant->commitPreparedTransaction(opCtx, optionalCommitTimestamp.get());
} else {
// commitUnpreparedTransaction will throw if the transaction is prepared.
- session->commitUnpreparedTransaction(opCtx);
+ txnParticipant->commitUnpreparedTransaction(opCtx);
}
return true;
@@ -139,16 +140,17 @@ public:
const std::string& dbname,
const BSONObj& cmdObj,
BSONObjBuilder& result) override {
- auto session = OperationContextSession::get(opCtx);
- uassert(
- ErrorCodes::CommandFailed, "prepareTransaction must be run within a session", session);
+ auto txnParticipant = TransactionParticipant::get(opCtx);
+ uassert(ErrorCodes::CommandFailed,
+ "prepareTransaction must be run within a transaction",
+ txnParticipant);
uassert(ErrorCodes::NoSuchTransaction,
"Transaction isn't in progress",
- session->inMultiDocumentTransaction());
+ txnParticipant->inMultiDocumentTransaction());
// Add prepareTimestamp to the command response.
- auto timestamp = session->prepareTransaction(opCtx);
+ auto timestamp = txnParticipant->prepareTransaction(opCtx);
result.append("prepareTimestamp", timestamp);
return true;
}
@@ -186,16 +188,16 @@ public:
const std::string& dbname,
const BSONObj& cmdObj,
BSONObjBuilder& result) override {
- auto session = OperationContextSession::get(opCtx);
- uassert(
- ErrorCodes::CommandFailed, "abortTransaction must be run within a session", session);
+ auto txnParticipant = TransactionParticipant::get(opCtx);
+ uassert(ErrorCodes::CommandFailed,
+ "abortTransaction must be run within a transaction",
+ txnParticipant);
- // TODO SERVER-33501 Change this when abortTransaction is retryable.
uassert(ErrorCodes::NoSuchTransaction,
"Transaction isn't in progress",
- session->inMultiDocumentTransaction());
+ txnParticipant->inMultiDocumentTransaction());
- session->abortActiveTransaction(opCtx);
+ txnParticipant->abortActiveTransaction(opCtx);
return true;
}
diff --git a/src/mongo/db/commands/write_commands/write_commands.cpp b/src/mongo/db/commands/write_commands/write_commands.cpp
index 35d2e0ec53b..65f0ec7c963 100644
--- a/src/mongo/db/commands/write_commands/write_commands.cpp
+++ b/src/mongo/db/commands/write_commands/write_commands.cpp
@@ -49,8 +49,8 @@
#include "mongo/db/repl/repl_client_info.h"
#include "mongo/db/repl/replication_coordinator.h"
#include "mongo/db/server_parameters.h"
-#include "mongo/db/session_catalog.h"
#include "mongo/db/stats/counters.h"
+#include "mongo/db/transaction_participant.h"
#include "mongo/db/write_concern.h"
#include "mongo/s/stale_exception.h"
@@ -256,8 +256,8 @@ private:
}
void _transactionChecks(OperationContext* opCtx) const {
- auto session = OperationContextSession::get(opCtx);
- if (!session || !session->inMultiDocumentTransaction())
+ auto txnParticipant = TransactionParticipant::get(opCtx);
+ if (!txnParticipant || !txnParticipant->inMultiDocumentTransaction())
return;
uassert(50791,
str::stream() << "Cannot write to system collection " << ns().toString()
diff --git a/src/mongo/db/db_raii.cpp b/src/mongo/db/db_raii.cpp
index 5b09a36981b..265c71aeb71 100644
--- a/src/mongo/db/db_raii.cpp
+++ b/src/mongo/db/db_raii.cpp
@@ -38,7 +38,7 @@
#include "mongo/db/repl/replication_coordinator.h"
#include "mongo/db/s/collection_sharding_state.h"
#include "mongo/db/server_parameters.h"
-#include "mongo/db/session_catalog.h"
+#include "mongo/db/transaction_participant.h"
#include "mongo/util/log.h"
namespace mongo {
@@ -323,8 +323,8 @@ LockMode getLockModeForQuery(OperationContext* opCtx) {
invariant(opCtx);
// Use IX locks for autocommit:false multi-statement transactions; otherwise, use IS locks.
- auto session = OperationContextSession::get(opCtx);
- if (session && session->inMultiDocumentTransaction()) {
+ auto txnParticipant = TransactionParticipant::get(opCtx);
+ if (txnParticipant && txnParticipant->inMultiDocumentTransaction()) {
return MODE_IX;
}
return MODE_IS;
diff --git a/src/mongo/db/kill_sessions_local.cpp b/src/mongo/db/kill_sessions_local.cpp
index 29562cc0a67..0c2b0cf9641 100644
--- a/src/mongo/db/kill_sessions_local.cpp
+++ b/src/mongo/db/kill_sessions_local.cpp
@@ -39,6 +39,7 @@
#include "mongo/db/service_context.h"
#include "mongo/db/session.h"
#include "mongo/db/session_catalog.h"
+#include "mongo/db/transaction_participant.h"
#include "mongo/util/log.h"
namespace mongo {
@@ -54,7 +55,8 @@ void killSessionsLocalKillTransactions(OperationContext* opCtx,
const SessionKiller::Matcher& matcher) {
SessionCatalog::get(opCtx)->scanSessions(
opCtx, matcher, [](OperationContext* opCtx, Session* session) {
- session->abortArbitraryTransaction();
+ TransactionParticipant::getFromNonCheckedOutSession(session)
+ ->abortArbitraryTransaction();
});
}
@@ -73,25 +75,15 @@ void killAllExpiredTransactions(OperationContext* opCtx) {
SessionCatalog::get(opCtx)->scanSessions(
opCtx, matcherAllSessions, [](OperationContext* opCtx, Session* session) {
try {
- session->abortArbitraryTransactionIfExpired();
+ TransactionParticipant::getFromNonCheckedOutSession(session)
+ ->abortArbitraryTransactionIfExpired();
} catch (const DBException& ex) {
Status status = ex.toStatus();
std::string errmsg = str::stream()
<< "May have failed to abort expired transaction with session id (lsid) '"
<< session->getSessionId() << "'."
<< " Caused by: " << status;
-
- // LockTimeout errors are expected if we are unable to acquire an IS lock to clean
- // up transaction cursors. The transaction abort (and lock resource release) should
- // have succeeded despite failing to clean up cursors. The cursors will eventually
- // be cleaned up by the cursor manager. We'll log such errors at a higher log level
- // for diagnostic purposes in case something gets stuck.
- if (ErrorCodes::isShutdownError(status.code()) ||
- status == ErrorCodes::LockTimeout) {
- LOG(1) << errmsg;
- } else {
- warning() << errmsg;
- }
+ warning() << errmsg;
}
});
}
diff --git a/src/mongo/db/op_observer_impl.cpp b/src/mongo/db/op_observer_impl.cpp
index 16b9701cd47..ccf3bc6e138 100644
--- a/src/mongo/db/op_observer_impl.cpp
+++ b/src/mongo/db/op_observer_impl.cpp
@@ -50,6 +50,7 @@
#include "mongo/db/s/shard_server_op_observer.h"
#include "mongo/db/server_options.h"
#include "mongo/db/session_catalog.h"
+#include "mongo/db/transaction_participant.h"
#include "mongo/db/views/durable_view_catalog.h"
#include "mongo/scripting/engine.h"
#include "mongo/util/assert_util.h"
@@ -371,9 +372,9 @@ void OpObserverImpl::onInserts(OperationContext* opCtx,
std::vector<InsertStatement>::const_iterator first,
std::vector<InsertStatement>::const_iterator last,
bool fromMigrate) {
- Session* const session = OperationContextSession::get(opCtx);
- const bool inMultiDocumentTransaction =
- session && opCtx->writesAreReplicated() && session->inMultiDocumentTransaction();
+ auto txnParticipant = TransactionParticipant::get(opCtx);
+ const bool inMultiDocumentTransaction = txnParticipant && opCtx->writesAreReplicated() &&
+ txnParticipant->inMultiDocumentTransaction();
Date_t lastWriteDate;
@@ -389,11 +390,12 @@ void OpObserverImpl::onInserts(OperationContext* opCtx,
}
for (auto iter = first; iter != last; iter++) {
auto operation = OplogEntry::makeInsertOperation(nss, uuid, iter->doc);
- session->addTransactionOperation(opCtx, operation);
+ txnParticipant->addTransactionOperation(opCtx, operation);
}
} else {
lastWriteDate = getWallClockTimeForOpLog(opCtx);
+ Session* const session = OperationContextSession::get(opCtx);
opTimeList =
repl::logInsertOps(opCtx, nss, uuid, session, first, last, fromMigrate, lastWriteDate);
if (!opTimeList.empty())
@@ -464,15 +466,16 @@ void OpObserverImpl::onUpdate(OperationContext* opCtx, const OplogUpdateEntryArg
return;
}
- Session* const session = OperationContextSession::get(opCtx);
- const bool inMultiDocumentTransaction =
- session && opCtx->writesAreReplicated() && session->inMultiDocumentTransaction();
+ auto txnParticipant = TransactionParticipant::get(opCtx);
+ const bool inMultiDocumentTransaction = txnParticipant && opCtx->writesAreReplicated() &&
+ txnParticipant->inMultiDocumentTransaction();
OpTimeBundle opTime;
if (inMultiDocumentTransaction) {
auto operation =
OplogEntry::makeUpdateOperation(args.nss, args.uuid, args.update, args.criteria);
- session->addTransactionOperation(opCtx, operation);
+ txnParticipant->addTransactionOperation(opCtx, operation);
} else {
+ Session* const session = OperationContextSession::get(opCtx);
opTime = replLogUpdate(opCtx, session, args);
onWriteOpCompleted(opCtx,
args.nss,
@@ -520,17 +523,18 @@ void OpObserverImpl::onDelete(OperationContext* opCtx,
StmtId stmtId,
bool fromMigrate,
const boost::optional<BSONObj>& deletedDoc) {
- Session* const session = OperationContextSession::get(opCtx);
auto& deleteState = getDeleteState(opCtx);
invariant(!deleteState.documentKey.isEmpty());
- const bool inMultiDocumentTransaction =
- session && opCtx->writesAreReplicated() && session->inMultiDocumentTransaction();
+ auto txnParticipant = TransactionParticipant::get(opCtx);
+ const bool inMultiDocumentTransaction = txnParticipant && opCtx->writesAreReplicated() &&
+ txnParticipant->inMultiDocumentTransaction();
OpTimeBundle opTime;
if (inMultiDocumentTransaction) {
auto operation = OplogEntry::makeDeleteOperation(
nss, uuid, deletedDoc ? deletedDoc.get() : deleteState.documentKey);
- session->addTransactionOperation(opCtx, operation);
+ txnParticipant->addTransactionOperation(opCtx, operation);
} else {
+ Session* const session = OperationContextSession::get(opCtx);
opTime = replLogDelete(opCtx, nss, uuid, session, stmtId, fromMigrate, deletedDoc);
onWriteOpCompleted(opCtx,
nss,
@@ -972,7 +976,7 @@ void OpObserverImpl::onTransactionCommit(OperationContext* opCtx, bool wasPrepar
// test the timestamping behavior.
const NamespaceString cmdNss{"admin", "$cmd"};
const auto cmdObj = BSON("commitTransaction" << 1);
- Session::SideTransactionBlock sideTxn(opCtx);
+ TransactionParticipant::SideTransactionBlock sideTxn(opCtx);
WriteUnitOfWork wuow(opCtx);
logOperation(opCtx,
"c",
@@ -989,15 +993,18 @@ void OpObserverImpl::onTransactionCommit(OperationContext* opCtx, bool wasPrepar
OplogSlot());
wuow.commit();
} else {
- Session* const session = OperationContextSession::get(opCtx);
- invariant(session);
- const auto stmts = session->endTransactionAndRetrieveOperations(opCtx);
+ auto txnParticipant = TransactionParticipant::get(opCtx);
+ invariant(txnParticipant);
+ const auto stmts = txnParticipant->endTransactionAndRetrieveOperations(opCtx);
// It is possible that the transaction resulted in no changes. In that case, we should
// not write an empty applyOps entry.
if (stmts.empty())
return;
+ Session* const session = OperationContextSession::get(opCtx);
+ invariant(session);
+
const auto commitOpTime =
logApplyOpsForTransaction(opCtx, session, stmts, OplogSlot()).writeOpTime;
invariant(!commitOpTime.isNull());
@@ -1006,19 +1013,23 @@ void OpObserverImpl::onTransactionCommit(OperationContext* opCtx, bool wasPrepar
void OpObserverImpl::onTransactionPrepare(OperationContext* opCtx, const OplogSlot& prepareOpTime) {
invariant(opCtx->getTxnNumber());
- Session* const session = OperationContextSession::get(opCtx);
- invariant(session);
- invariant(session->inMultiDocumentTransaction());
+ auto txnParticipant = TransactionParticipant::get(opCtx);
+ invariant(txnParticipant);
+ invariant(txnParticipant->inMultiDocumentTransaction());
invariant(!prepareOpTime.opTime.isNull());
- auto stmts = session->endTransactionAndRetrieveOperations(opCtx);
+ auto stmts = txnParticipant->endTransactionAndRetrieveOperations(opCtx);
// We write the oplog entry in a side transaction so that we do not commit the now-prepared
// transaction.
// We write an empty 'applyOps' entry if there were no writes to choose a prepare timestamp
// and allow this transaction to be continued on failover.
{
- Session::SideTransactionBlock sideTxn(opCtx);
+ TransactionParticipant::SideTransactionBlock sideTxn(opCtx);
WriteUnitOfWork wuow(opCtx);
+
+ Session* const session = OperationContextSession::get(opCtx);
+ invariant(session);
+
logApplyOpsForTransaction(opCtx, session, stmts, prepareOpTime);
wuow.commit();
}
diff --git a/src/mongo/db/op_observer_impl_test.cpp b/src/mongo/db/op_observer_impl_test.cpp
index 048b4491dae..3bf8a4bf967 100644
--- a/src/mongo/db/op_observer_impl_test.cpp
+++ b/src/mongo/db/op_observer_impl_test.cpp
@@ -36,6 +36,7 @@
#include "mongo/db/keys_collection_manager.h"
#include "mongo/db/logical_clock.h"
#include "mongo/db/logical_time_validator.h"
+#include "mongo/db/operation_context_session_mongod.h"
#include "mongo/db/repl/oplog.h"
#include "mongo/db/repl/oplog_interface_local.h"
#include "mongo/db/repl/repl_client_info.h"
@@ -43,6 +44,7 @@
#include "mongo/db/service_context_d_test_fixture.h"
#include "mongo/db/session_catalog.h"
#include "mongo/db/storage/ephemeral_for_test/ephemeral_for_test_recovery_unit.h"
+#include "mongo/db/transaction_participant.h"
#include "mongo/s/config_server_test_fixture.h"
#include "mongo/unittest/death_test.h"
#include "mongo/util/clock_source_mock.h"
@@ -312,7 +314,7 @@ public:
NamespaceString nss,
TxnNumber txnNum,
StmtId stmtId) {
- session->beginOrContinueTxn(opCtx, txnNum, boost::none, boost::none, "testDB", "insert");
+ session->beginOrContinueTxn(opCtx, txnNum);
{
AutoGetCollection autoColl(opCtx, nss, MODE_IX);
@@ -333,6 +335,7 @@ TEST_F(OpObserverSessionCatalogTest, OnRollbackInvalidatesSessionCatalogIfSessio
auto sessionCatalog = SessionCatalog::get(getServiceContext());
auto sessionId = makeLogicalSessionIdForTest();
auto session = sessionCatalog->getOrCreateSession(opCtx.get(), sessionId);
+ session->refreshFromStorageIfNeeded(opCtx.get());
// Simulate a write occurring on that session.
const TxnNumber txnNum = 0;
@@ -362,6 +365,7 @@ TEST_F(OpObserverSessionCatalogTest,
auto sessionCatalog = SessionCatalog::get(getServiceContext());
auto sessionId = makeLogicalSessionIdForTest();
auto session = sessionCatalog->getOrCreateSession(opCtx.get(), sessionId);
+ session->refreshFromStorageIfNeeded(opCtx.get());
// Simulate a write occurring on that session.
const TxnNumber txnNum = 0;
@@ -409,14 +413,10 @@ TEST_F(OpObserverLargeTransactionTest, TransactionTooLargeWhileCommitting) {
const TxnNumber txnNum = 0;
opCtx->setLogicalSessionId(sessionId);
opCtx->setTxnNumber(txnNum);
- OperationContextSession opSession(opCtx.get(),
- true /* checkOutSession */,
- false /* autocommit */,
- true /* startTransaction */,
- "testDB" /* dbName */,
- "insert" /* cmdName */);
- session->unstashTransactionResources(opCtx.get(), "insert");
+ OperationContextSessionMongod opSession(opCtx.get(), true, false, true);
+ auto txnParticipant = TransactionParticipant::get(opCtx.get());
+ txnParticipant->unstashTransactionResources(opCtx.get(), "insert");
// This size is crafted such that two operations of this size are not too big to fit in a single
// oplog entry, but two operations plus oplog overhead are too big to fit in a single oplog
@@ -429,9 +429,9 @@ TEST_F(OpObserverLargeTransactionTest, TransactionTooLargeWhileCommitting) {
BSON(
"_id" << 0 << "data"
<< BSONBinData(halfTransactionData.get(), kHalfTransactionSize, BinDataGeneral)));
- session->addTransactionOperation(opCtx.get(), operation);
- session->addTransactionOperation(opCtx.get(), operation);
- session->transitionToCommittingforTest();
+ txnParticipant->addTransactionOperation(opCtx.get(), operation);
+ txnParticipant->addTransactionOperation(opCtx.get(), operation);
+ txnParticipant->transitionToCommittingforTest();
ASSERT_THROWS_CODE(opObserver.onTransactionCommit(opCtx.get(), false),
AssertionException,
ErrorCodes::TransactionTooLarge);
@@ -502,6 +502,7 @@ public:
auto sessionCatalog = SessionCatalog::get(getServiceContext());
auto sessionId = makeLogicalSessionIdForTest();
_session = sessionCatalog->getOrCreateSession(opCtx(), sessionId);
+ _session->get()->refreshFromStorageIfNeeded(opCtx());
opCtx()->setLogicalSessionId(sessionId);
_opObserver.emplace();
_times.emplace(opCtx());
@@ -554,14 +555,11 @@ TEST_F(OpObserverTransactionTest, TransactionalPrepareTest) {
auto uuid2 = CollectionUUID::gen();
const TxnNumber txnNum = 2;
opCtx()->setTxnNumber(txnNum);
- OperationContextSession opSession(opCtx(),
- true /* checkOutSession */,
- false /* autocommit */,
- true /* startTransaction*/,
- "testDB",
- "insert");
-
- session()->unstashTransactionResources(opCtx(), "insert");
+
+ OperationContextSessionMongod opSession(opCtx(), true, false, true);
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+ txnParticipant->unstashTransactionResources(opCtx(), "insert");
+
WriteUnitOfWork wuow(opCtx());
AutoGetCollection autoColl1(opCtx(), nss1, MODE_IX);
AutoGetCollection autoColl2(opCtx(), nss2, MODE_IX);
@@ -592,7 +590,7 @@ TEST_F(OpObserverTransactionTest, TransactionalPrepareTest) {
<< "x"));
opObserver().onDelete(opCtx(), nss1, uuid1, 0, false, boost::none);
- session()->transitionToPreparedforTest();
+ txnParticipant->transitionToPreparedforTest();
{
WriteUnitOfWork wuow(opCtx());
OplogSlot slot = repl::getNextOpTime(opCtx());
@@ -650,15 +648,12 @@ TEST_F(OpObserverTransactionTest, TransactionalPrepareTest) {
TEST_F(OpObserverTransactionTest, PreparingEmptyTransactionLogsEmptyApplyOps) {
const TxnNumber txnNum = 2;
opCtx()->setTxnNumber(txnNum);
- OperationContextSession opSession(opCtx(),
- true /* checkOutSession */,
- false /* autocommit */,
- true /* startTransaction*/,
- "admin",
- "prepareTransaction");
-
- session()->unstashTransactionResources(opCtx(), "prepareTransaction");
- session()->transitionToPreparedforTest();
+
+ OperationContextSessionMongod opSession(opCtx(), true, false, true);
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+ txnParticipant->unstashTransactionResources(opCtx(), "prepareTransaction");
+ txnParticipant->transitionToPreparedforTest();
+
{
WriteUnitOfWork wuow(opCtx());
OplogSlot slot = repl::getNextOpTime(opCtx());
@@ -684,14 +679,10 @@ TEST_F(OpObserverTransactionTest, TransactionalInsertTest) {
auto uuid2 = CollectionUUID::gen();
const TxnNumber txnNum = 2;
opCtx()->setTxnNumber(txnNum);
- OperationContextSession opSession(opCtx(),
- true /* checkOutSession */,
- false /* autocommit */,
- true /* startTransaction*/,
- "testDB",
- "insert");
- session()->unstashTransactionResources(opCtx(), "insert");
+ OperationContextSessionMongod opSession(opCtx(), true, false, true);
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+ txnParticipant->unstashTransactionResources(opCtx(), "insert");
std::vector<InsertStatement> inserts1;
inserts1.emplace_back(0,
@@ -712,7 +703,7 @@ TEST_F(OpObserverTransactionTest, TransactionalInsertTest) {
AutoGetCollection autoColl2(opCtx(), nss2, MODE_IX);
opObserver().onInserts(opCtx(), nss1, uuid1, inserts1.begin(), inserts1.end(), false);
opObserver().onInserts(opCtx(), nss2, uuid2, inserts2.begin(), inserts2.end(), false);
- session()->transitionToCommittingforTest();
+ txnParticipant->transitionToCommittingforTest();
opObserver().onTransactionCommit(opCtx(), false);
auto oplogEntryObj = getSingleOplogEntry(opCtx());
checkCommonFields(oplogEntryObj);
@@ -766,14 +757,10 @@ TEST_F(OpObserverTransactionTest, TransactionalUpdateTest) {
auto uuid2 = CollectionUUID::gen();
const TxnNumber txnNum = 3;
opCtx()->setTxnNumber(txnNum);
- OperationContextSession opSession(opCtx(),
- true /* checkOutSession */,
- false /* autocommit */,
- true /* startTransaction*/,
- "testDB",
- "update");
- session()->unstashTransactionResources(opCtx(), "update");
+ OperationContextSessionMongod opSession(opCtx(), true, false, true);
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+ txnParticipant->unstashTransactionResources(opCtx(), "update");
OplogUpdateEntryArgs update1;
update1.nss = nss1;
@@ -800,7 +787,7 @@ TEST_F(OpObserverTransactionTest, TransactionalUpdateTest) {
AutoGetCollection autoColl2(opCtx(), nss2, MODE_IX);
opObserver().onUpdate(opCtx(), update1);
opObserver().onUpdate(opCtx(), update2);
- session()->transitionToCommittingforTest();
+ txnParticipant->transitionToCommittingforTest();
opObserver().onTransactionCommit(opCtx(), false);
auto oplogEntry = getSingleOplogEntry(opCtx());
checkCommonFields(oplogEntry);
@@ -839,14 +826,11 @@ TEST_F(OpObserverTransactionTest, TransactionalDeleteTest) {
auto uuid2 = CollectionUUID::gen();
const TxnNumber txnNum = 3;
opCtx()->setTxnNumber(txnNum);
- OperationContextSession opSession(opCtx(),
- true /* checkOutSession */,
- false /* autocommit */,
- true /* startTransaction*/,
- "testDB",
- "delete");
- session()->unstashTransactionResources(opCtx(), "delete");
+ OperationContextSessionMongod sessionTxnState(opCtx(), true, false, true);
+
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+ txnParticipant->unstashTransactionResources(opCtx(), "delete");
WriteUnitOfWork wuow(opCtx());
AutoGetCollection autoColl1(opCtx(), nss1, MODE_IX);
@@ -861,7 +845,7 @@ TEST_F(OpObserverTransactionTest, TransactionalDeleteTest) {
BSON("_id" << 1 << "data"
<< "y"));
opObserver().onDelete(opCtx(), nss2, uuid2, 0, false, boost::none);
- session()->transitionToCommittingforTest();
+ txnParticipant->transitionToCommittingforTest();
opObserver().onTransactionCommit(opCtx(), false);
auto oplogEntry = getSingleOplogEntry(opCtx());
checkCommonFields(oplogEntry);
diff --git a/src/mongo/db/operation_context_session_mongod.cpp b/src/mongo/db/operation_context_session_mongod.cpp
new file mode 100644
index 00000000000..a7827e6ad64
--- /dev/null
+++ b/src/mongo/db/operation_context_session_mongod.cpp
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2018 MongoDB, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * As a special exception, the copyright holders give permission to link the
+ * code of portions of this program with the OpenSSL library under certain
+ * conditions as described in each individual source file and distribute
+ * linked combinations including the program with the OpenSSL library. You
+ * must comply with the GNU Affero General Public License in all respects for
+ * all of the code used other than as permitted herein. If you modify file(s)
+ * with this exception, you may extend this exception to your version of the
+ * file(s), but you are not obligated to do so. If you do not wish to do so,
+ * delete this exception statement from your version. If you delete this
+ * exception statement from all source files in the program, then also delete
+ * it in the license file.
+ */
+
+#include "mongo/db/operation_context_session_mongod.h"
+
+#include "mongo/db/transaction_participant.h"
+
+namespace mongo {
+
+OperationContextSessionMongod::OperationContextSessionMongod(OperationContext* opCtx,
+ bool shouldCheckOutSession,
+ boost::optional<bool> autocommit,
+ boost::optional<bool> startTransaction)
+ : _operationContextSession(opCtx, shouldCheckOutSession) {
+ if (shouldCheckOutSession && !opCtx->getClient()->isInDirectClient()) {
+ auto session = OperationContextSession::get(opCtx);
+ invariant(session);
+
+ auto clientTxnNumber = *opCtx->getTxnNumber();
+ session->refreshFromStorageIfNeeded(opCtx);
+ session->beginOrContinueTxn(opCtx, clientTxnNumber);
+
+ auto txnParticipant = TransactionParticipant::get(opCtx);
+ txnParticipant->beginOrContinue(clientTxnNumber, autocommit, startTransaction);
+ }
+}
+
+} // namespace mongo
diff --git a/src/mongo/db/operation_context_session_mongod.h b/src/mongo/db/operation_context_session_mongod.h
new file mode 100644
index 00000000000..2991d223da7
--- /dev/null
+++ b/src/mongo/db/operation_context_session_mongod.h
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2018 MongoDB, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * As a special exception, the copyright holders give permission to link the
+ * code of portions of this program with the OpenSSL library under certain
+ * conditions as described in each individual source file and distribute
+ * linked combinations including the program with the OpenSSL library. You
+ * must comply with the GNU Affero General Public License in all respects for
+ * all of the code used other than as permitted herein. If you modify file(s)
+ * with this exception, you may extend this exception to your version of the
+ * file(s), but you are not obligated to do so. If you do not wish to do so,
+ * delete this exception statement from your version. If you delete this
+ * exception statement from all source files in the program, then also delete
+ * it in the license file.
+ */
+
+#pragma once
+
+#include <boost/optional.hpp>
+
+#include "mongo/db/session_catalog.h"
+
+namespace mongo {
+
+class OperationContext;
+
+/**
+ * Scoped object, which checks out the session specified in the passed operation context and stores
+ * it for later access by the command. The session is installed at construction time and is removed
+ * at destruction.
+ */
+class OperationContextSessionMongod {
+public:
+ OperationContextSessionMongod(OperationContext* opCtx,
+ bool shouldCheckOutSession,
+ boost::optional<bool> autocommit,
+ boost::optional<bool> startTransaction);
+
+private:
+ OperationContextSession _operationContextSession;
+};
+
+} // namespace mongo
diff --git a/src/mongo/db/ops/write_ops_exec.cpp b/src/mongo/db/ops/write_ops_exec.cpp
index e36d1b071a9..85411e0106d 100644
--- a/src/mongo/db/ops/write_ops_exec.cpp
+++ b/src/mongo/db/ops/write_ops_exec.cpp
@@ -69,6 +69,7 @@
#include "mongo/db/session_catalog.h"
#include "mongo/db/stats/counters.h"
#include "mongo/db/stats/top.h"
+#include "mongo/db/transaction_participant.h"
#include "mongo/db/write_concern.h"
#include "mongo/rpc/get_status_from_command_result.h"
#include "mongo/s/cannot_implicitly_create_collection_info.h"
@@ -127,7 +128,7 @@ void finishCurOp(OperationContext* opCtx, CurOp* curOp) {
if (curOp->shouldDBProfile(shouldSample)) {
// Stash the current transaction so that writes to the profile collection are not
// done as part of the transaction.
- Session::SideTransactionBlock sideTxn(opCtx);
+ TransactionParticipant::SideTransactionBlock sideTxn(opCtx);
profile(opCtx, CurOp::get(opCtx)->getNetworkOp());
}
} catch (const DBException& ex) {
@@ -189,8 +190,8 @@ void assertCanWrite_inlock(OperationContext* opCtx, const NamespaceString& ns) {
}
void makeCollection(OperationContext* opCtx, const NamespaceString& ns) {
- auto session = OperationContextSession::get(opCtx);
- auto inTransaction = session && session->inMultiDocumentTransaction();
+ auto txnParticipant = TransactionParticipant::get(opCtx);
+ auto inTransaction = txnParticipant && txnParticipant->inMultiDocumentTransaction();
uassert(ErrorCodes::OperationNotSupportedInTransaction,
str::stream() << "Cannot create namespace " << ns.ns()
<< " in multi-document transaction.",
@@ -227,8 +228,8 @@ bool handleError(OperationContext* opCtx,
throw; // These have always failed the whole batch.
}
- auto session = OperationContextSession::get(opCtx);
- if (session && session->inMultiDocumentTransaction()) {
+ auto txnParticipant = TransactionParticipant::get(opCtx);
+ if (txnParticipant && txnParticipant->inMultiDocumentTransaction()) {
// If we are in a transaction, we must fail the whole batch.
throw;
}
@@ -336,8 +337,8 @@ void insertDocuments(OperationContext* opCtx,
auto batchSize = std::distance(begin, end);
if (supportsDocLocking()) {
auto replCoord = repl::ReplicationCoordinator::get(opCtx);
- auto session = OperationContextSession::get(opCtx);
- auto inTransaction = session && session->inMultiDocumentTransaction();
+ auto txnParticipant = TransactionParticipant::get(opCtx);
+ auto inTransaction = txnParticipant && txnParticipant->inMultiDocumentTransaction();
if (!inTransaction && !replCoord->isOplogDisabledFor(opCtx, collection->ns())) {
// Populate 'slots' with new optimes for each insert.
@@ -481,9 +482,9 @@ WriteResult performInserts(OperationContext* opCtx,
bool fromMigrate) {
// Insert performs its own retries, so we should only be within a WriteUnitOfWork when run in a
// transaction.
- auto session = OperationContextSession::get(opCtx);
+ auto txnParticipant = TransactionParticipant::get(opCtx);
invariant(!opCtx->lockState()->inAWriteUnitOfWork() ||
- (session && session->inMultiDocumentTransaction()));
+ (txnParticipant && txnParticipant->inMultiDocumentTransaction()));
auto& curOp = *CurOp::get(opCtx);
ON_BLOCK_EXIT([&] {
// This is the only part of finishCurOp we need to do for inserts because they reuse the
@@ -542,7 +543,8 @@ WriteResult performInserts(OperationContext* opCtx,
if (opCtx->getTxnNumber()) {
auto session = OperationContextSession::get(opCtx);
invariant(session);
- if (session->checkStatementExecutedNoOplogEntryFetch(*opCtx->getTxnNumber(),
+ if (!txnParticipant->inMultiDocumentTransaction() &&
+ session->checkStatementExecutedNoOplogEntryFetch(*opCtx->getTxnNumber(),
stmtId)) {
containsRetry = true;
RetryableWritesStats::get(opCtx)->incrementRetriedStatementsCount();
@@ -585,11 +587,11 @@ static SingleWriteResult performSingleUpdateOp(OperationContext* opCtx,
const NamespaceString& ns,
StmtId stmtId,
const write_ops::UpdateOpEntry& op) {
- auto session = OperationContextSession::get(opCtx);
+ auto txnParticipant = TransactionParticipant::get(opCtx);
uassert(ErrorCodes::InvalidOptions,
"Cannot use (or request) retryable writes with multi=true",
- (session && session->inMultiDocumentTransaction()) || !opCtx->getTxnNumber() ||
- !op.getMulti());
+ (txnParticipant && txnParticipant->inMultiDocumentTransaction()) ||
+ !opCtx->getTxnNumber() || !op.getMulti());
globalOpCounters.gotUpdate();
auto& curOp = *CurOp::get(opCtx);
@@ -687,9 +689,9 @@ static SingleWriteResult performSingleUpdateOp(OperationContext* opCtx,
WriteResult performUpdates(OperationContext* opCtx, const write_ops::Update& wholeOp) {
// Update performs its own retries, so we should not be in a WriteUnitOfWork unless run in a
// transaction.
- auto session = OperationContextSession::get(opCtx);
+ auto txnParticipant = TransactionParticipant::get(opCtx);
invariant(!opCtx->lockState()->inAWriteUnitOfWork() ||
- (session && session->inMultiDocumentTransaction()));
+ (txnParticipant && txnParticipant->inMultiDocumentTransaction()));
uassertStatusOK(userAllowedWriteNS(wholeOp.getNamespace()));
DisableDocumentValidationIfTrue docValidationDisabler(
@@ -707,12 +709,14 @@ WriteResult performUpdates(OperationContext* opCtx, const write_ops::Update& who
const auto stmtId = getStmtIdForWriteOp(opCtx, wholeOp, stmtIdIndex++);
if (opCtx->getTxnNumber()) {
auto session = OperationContextSession::get(opCtx);
- if (auto entry =
- session->checkStatementExecuted(opCtx, *opCtx->getTxnNumber(), stmtId)) {
- containsRetry = true;
- RetryableWritesStats::get(opCtx)->incrementRetriedStatementsCount();
- out.results.emplace_back(parseOplogEntryForUpdate(*entry));
- continue;
+ if (!txnParticipant->inMultiDocumentTransaction()) {
+ if (auto entry =
+ session->checkStatementExecuted(opCtx, *opCtx->getTxnNumber(), stmtId)) {
+ containsRetry = true;
+ RetryableWritesStats::get(opCtx)->incrementRetriedStatementsCount();
+ out.results.emplace_back(parseOplogEntryForUpdate(*entry));
+ continue;
+ }
}
}
@@ -746,11 +750,11 @@ static SingleWriteResult performSingleDeleteOp(OperationContext* opCtx,
const NamespaceString& ns,
StmtId stmtId,
const write_ops::DeleteOpEntry& op) {
- auto session = OperationContextSession::get(opCtx);
+ auto txnParticipant = TransactionParticipant::get(opCtx);
uassert(ErrorCodes::InvalidOptions,
"Cannot use (or request) retryable writes with limit=0",
- (session && session->inMultiDocumentTransaction()) || !opCtx->getTxnNumber() ||
- !op.getMulti());
+ (txnParticipant && txnParticipant->inMultiDocumentTransaction()) ||
+ !opCtx->getTxnNumber() || !op.getMulti());
globalOpCounters.gotDelete();
auto& curOp = *CurOp::get(opCtx);
@@ -828,9 +832,9 @@ static SingleWriteResult performSingleDeleteOp(OperationContext* opCtx,
WriteResult performDeletes(OperationContext* opCtx, const write_ops::Delete& wholeOp) {
// Delete performs its own retries, so we should not be in a WriteUnitOfWork unless we are in a
// transaction.
- auto session = OperationContextSession::get(opCtx);
+ auto txnParticipant = TransactionParticipant::get(opCtx);
invariant(!opCtx->lockState()->inAWriteUnitOfWork() ||
- (session && session->inMultiDocumentTransaction()));
+ (txnParticipant && txnParticipant->inMultiDocumentTransaction()));
uassertStatusOK(userAllowedWriteNS(wholeOp.getNamespace()));
DisableDocumentValidationIfTrue docValidationDisabler(
@@ -848,7 +852,8 @@ WriteResult performDeletes(OperationContext* opCtx, const write_ops::Delete& who
const auto stmtId = getStmtIdForWriteOp(opCtx, wholeOp, stmtIdIndex++);
if (opCtx->getTxnNumber()) {
auto session = OperationContextSession::get(opCtx);
- if (session->checkStatementExecutedNoOplogEntryFetch(*opCtx->getTxnNumber(), stmtId)) {
+ if (!txnParticipant->inMultiDocumentTransaction() &&
+ session->checkStatementExecutedNoOplogEntryFetch(*opCtx->getTxnNumber(), stmtId)) {
containsRetry = true;
RetryableWritesStats::get(opCtx)->incrementRetriedStatementsCount();
out.results.emplace_back(makeWriteResultForInsertOrDeleteRetry());
diff --git a/src/mongo/db/periodic_runner_job_abort_expired_transactions.cpp b/src/mongo/db/periodic_runner_job_abort_expired_transactions.cpp
index 196a0f1dca9..20efff83a51 100644
--- a/src/mongo/db/periodic_runner_job_abort_expired_transactions.cpp
+++ b/src/mongo/db/periodic_runner_job_abort_expired_transactions.cpp
@@ -35,7 +35,7 @@
#include "mongo/db/client.h"
#include "mongo/db/kill_sessions_local.h"
#include "mongo/db/service_context.h"
-#include "mongo/db/session.h"
+#include "mongo/db/transaction_participant.h"
#include "mongo/util/log.h"
#include "mongo/util/periodic_runner.h"
diff --git a/src/mongo/db/pipeline/mongod_process_interface.cpp b/src/mongo/db/pipeline/mongod_process_interface.cpp
index e54ceb1dc17..8e52cf4d057 100644
--- a/src/mongo/db/pipeline/mongod_process_interface.cpp
+++ b/src/mongo/db/pipeline/mongod_process_interface.cpp
@@ -47,6 +47,7 @@
#include "mongo/db/session_catalog.h"
#include "mongo/db/stats/fill_locker_info.h"
#include "mongo/db/stats/storage_stats.h"
+#include "mongo/db/transaction_participant.h"
#include "mongo/s/catalog_cache.h"
#include "mongo/s/grid.h"
#include "mongo/s/write_ops/cluster_write.h"
@@ -404,8 +405,8 @@ BSONObj MongoDInterface::_reportCurrentOpForClient(OperationContext* opCtx,
OperationContext* clientOpCtx = client->getOperationContext();
if (clientOpCtx) {
- if (auto opCtxSession = OperationContextSession::get(clientOpCtx)) {
- opCtxSession->reportUnstashedState(repl::ReadConcernArgs::get(clientOpCtx), &builder);
+ if (auto txnParticipant = TransactionParticipant::get(clientOpCtx)) {
+ txnParticipant->reportUnstashedState(repl::ReadConcernArgs::get(clientOpCtx), &builder);
}
// Append lock stats before returning.
@@ -433,14 +434,16 @@ void MongoDInterface::_reportCurrentOpsForIdleSessions(OperationContext* opCtx,
? makeSessionFilterForAuthenticatedUsers(opCtx)
: KillAllSessionsByPatternSet{{}});
- sessionCatalog->scanSessions(opCtx,
- {std::move(sessionFilter)},
- [&](OperationContext* opCtx, Session* session) {
- auto op = session->reportStashedState();
- if (!op.isEmpty()) {
- ops->emplace_back(op);
- }
- });
+ sessionCatalog->scanSessions(
+ opCtx,
+ {std::move(sessionFilter)},
+ [&](OperationContext* opCtx, Session* session) {
+ auto op =
+ TransactionParticipant::getFromNonCheckedOutSession(session)->reportStashedState();
+ if (!op.isEmpty()) {
+ ops->emplace_back(op);
+ }
+ });
}
std::unique_ptr<CollatorInterface> MongoDInterface::_getCollectionDefaultCollator(
diff --git a/src/mongo/db/pipeline/pipeline_d.cpp b/src/mongo/db/pipeline/pipeline_d.cpp
index e7db3deeae7..e699ee96ace 100644
--- a/src/mongo/db/pipeline/pipeline_d.cpp
+++ b/src/mongo/db/pipeline/pipeline_d.cpp
@@ -72,6 +72,7 @@
#include "mongo/db/stats/top.h"
#include "mongo/db/storage/record_store.h"
#include "mongo/db/storage/sorted_data_interface.h"
+#include "mongo/db/transaction_participant.h"
#include "mongo/rpc/metadata/client_metadata_ismaster.h"
#include "mongo/s/catalog_cache.h"
#include "mongo/s/chunk_manager.h"
diff --git a/src/mongo/db/read_concern.cpp b/src/mongo/db/read_concern.cpp
index f1130eb6942..cb8df144d25 100644
--- a/src/mongo/db/read_concern.cpp
+++ b/src/mongo/db/read_concern.cpp
@@ -47,7 +47,7 @@
#include "mongo/db/s/sharding_state.h"
#include "mongo/db/server_options.h"
#include "mongo/db/server_parameters.h"
-#include "mongo/db/session_catalog.h"
+#include "mongo/db/transaction_participant.h"
#include "mongo/s/client/shard_registry.h"
#include "mongo/s/grid.h"
#include "mongo/util/log.h"
@@ -207,9 +207,9 @@ Status waitForReadConcern(OperationContext* opCtx,
// If we are in a direct client within a transaction, then we may be holding locks, so it is
// illegal to wait for read concern. This is fine, since the outer operation should have handled
// waiting for read concern.
- auto session = OperationContextSession::get(opCtx);
- if (opCtx->getClient()->isInDirectClient() && session &&
- session->inMultiDocumentTransaction()) {
+ auto txnParticipant = TransactionParticipant::get(opCtx);
+ if (opCtx->getClient()->isInDirectClient() && txnParticipant &&
+ txnParticipant->inMultiDocumentTransaction()) {
return Status::OK();
}
@@ -220,8 +220,8 @@ Status waitForReadConcern(OperationContext* opCtx,
// concern is not yet supported with atClusterTime.
//
// TODO SERVER-34620: Re-enable speculative behavior when "atClusterTime" is specified.
- const bool speculative =
- session && session->inMultiDocumentTransaction() && !readConcernArgs.getArgsAtClusterTime();
+ const bool speculative = txnParticipant && txnParticipant->inMultiDocumentTransaction() &&
+ !readConcernArgs.getArgsAtClusterTime();
if (readConcernArgs.getLevel() == repl::ReadConcernLevel::kLinearizableReadConcern) {
if (replCoord->getReplicationMode() != repl::ReplicationCoordinator::modeReplSet) {
@@ -295,7 +295,7 @@ Status waitForReadConcern(OperationContext* opCtx,
"node needs to be a replica set member to use readConcern: snapshot"};
}
if (speculative) {
- session->setSpeculativeTransactionOpTimeToLastApplied(opCtx);
+ txnParticipant->setSpeculativeTransactionOpTimeToLastApplied(opCtx);
}
}
diff --git a/src/mongo/db/repl/do_txn.cpp b/src/mongo/db/repl/do_txn.cpp
index 548d029e991..3d16c28f01c 100644
--- a/src/mongo/db/repl/do_txn.cpp
+++ b/src/mongo/db/repl/do_txn.cpp
@@ -51,6 +51,7 @@
#include "mongo/db/repl/replication_coordinator.h"
#include "mongo/db/service_context.h"
#include "mongo/db/session_catalog.h"
+#include "mongo/db/transaction_participant.h"
#include "mongo/rpc/get_status_from_command_result.h"
#include "mongo/util/fail_point_service.h"
#include "mongo/util/log.h"
@@ -270,9 +271,9 @@ Status doTxn(OperationContext* opCtx,
BSONObjBuilder* result) {
auto txnNumber = opCtx->getTxnNumber();
uassert(ErrorCodes::InvalidOptions, "doTxn can only be run with a transaction ID.", txnNumber);
- auto* session = OperationContextSession::get(opCtx);
- uassert(ErrorCodes::InvalidOptions, "doTxn must be run within a session", session);
- invariant(session->inMultiDocumentTransaction());
+ auto txnParticipant = TransactionParticipant::get(opCtx);
+ uassert(ErrorCodes::InvalidOptions, "doTxn must be run within a transaction", txnParticipant);
+ invariant(txnParticipant->inMultiDocumentTransaction());
invariant(opCtx->getWriteUnitOfWork());
uassert(
ErrorCodes::InvalidOptions, "doTxn supports only CRUD opts.", _areOpsCrudOnly(doTxnCmd));
@@ -303,10 +304,10 @@ Status doTxn(OperationContext* opCtx,
numApplied = 0;
uassertStatusOK(_doTxn(opCtx, dbName, doTxnCmd, &intermediateResult, &numApplied));
- session->commitUnpreparedTransaction(opCtx);
+ txnParticipant->commitUnpreparedTransaction(opCtx);
result->appendElements(intermediateResult.obj());
} catch (const DBException& ex) {
- session->abortActiveTransaction(opCtx);
+ txnParticipant->abortActiveTransaction(opCtx);
BSONArrayBuilder ab;
++numApplied;
for (int j = 0; j < numApplied; j++)
diff --git a/src/mongo/db/repl/do_txn_test.cpp b/src/mongo/db/repl/do_txn_test.cpp
index 735222094ff..7a15e2bde71 100644
--- a/src/mongo/db/repl/do_txn_test.cpp
+++ b/src/mongo/db/repl/do_txn_test.cpp
@@ -33,6 +33,7 @@
#include "mongo/db/op_observer_impl.h"
#include "mongo/db/op_observer_noop.h"
#include "mongo/db/op_observer_registry.h"
+#include "mongo/db/operation_context_session_mongod.h"
#include "mongo/db/repl/do_txn.h"
#include "mongo/db/repl/oplog_interface_local.h"
#include "mongo/db/repl/repl_client_info.h"
@@ -40,6 +41,7 @@
#include "mongo/db/repl/storage_interface_impl.h"
#include "mongo/db/service_context_d_test_fixture.h"
#include "mongo/db/session_catalog.h"
+#include "mongo/db/transaction_participant.h"
#include "mongo/logger/logger.h"
#include "mongo/rpc/get_status_from_command_result.h"
#include "mongo/stdx/memory.h"
@@ -105,7 +107,7 @@ protected:
OpObserverMock* _opObserver = nullptr;
std::unique_ptr<StorageInterface> _storage;
ServiceContext::UniqueOperationContext _opCtx;
- boost::optional<OperationContextSession> _ocs;
+ boost::optional<OperationContextSessionMongod> _ocs;
};
void DoTxnTest::setUp() {
@@ -145,13 +147,10 @@ void DoTxnTest::setUp() {
// Set up the transaction and session.
_opCtx->setLogicalSessionId(makeLogicalSessionIdForTest());
_opCtx->setTxnNumber(0); // TxnNumber can always be 0 because we have a new session.
- _ocs.emplace(_opCtx.get(),
- true /* checkOutSession */,
- false /* autocommit */,
- true /* startTransaction */,
- "admin" /* dbName */,
- "doTxn" /* cmdName */);
- OperationContextSession::get(opCtx())->unstashTransactionResources(opCtx(), "doTxn");
+ _ocs.emplace(_opCtx.get(), true /* checkOutSession */, false, true);
+
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+ txnParticipant->unstashTransactionResources(opCtx(), "doTxn");
}
void DoTxnTest::tearDown() {
diff --git a/src/mongo/db/repl/replication_coordinator_impl.cpp b/src/mongo/db/repl/replication_coordinator_impl.cpp
index 352dae2d3c4..0ab12ce6e63 100644
--- a/src/mongo/db/repl/replication_coordinator_impl.cpp
+++ b/src/mongo/db/repl/replication_coordinator_impl.cpp
@@ -68,7 +68,7 @@
#include "mongo/db/repl/vote_requester.h"
#include "mongo/db/server_options.h"
#include "mongo/db/server_parameters.h"
-#include "mongo/db/session_catalog.h"
+#include "mongo/db/transaction_participant.h"
#include "mongo/db/write_concern.h"
#include "mongo/db/write_concern_options.h"
#include "mongo/executor/connection_pool_stats.h"
@@ -1349,9 +1349,9 @@ Status ReplicationCoordinatorImpl::_waitUntilClusterTimeForRead(OperationContext
invariant(!readConcern.getArgsOpTime());
// TODO SERVER-34620: Re-enable speculative behavior when "atClusterTime" is specified.
- auto session = OperationContextSession::get(opCtx);
- const bool speculative =
- session && session->inMultiDocumentTransaction() && !readConcern.getArgsAtClusterTime();
+ auto txnParticipant = TransactionParticipant::get(opCtx);
+ const bool speculative = txnParticipant && txnParticipant->inMultiDocumentTransaction() &&
+ !readConcern.getArgsAtClusterTime();
const bool isMajorityCommittedRead =
(readConcern.getLevel() == ReadConcernLevel::kMajorityReadConcern ||
@@ -1927,8 +1927,8 @@ Status ReplicationCoordinatorImpl::checkCanServeReadsFor_UNSAFE(OperationContext
return Status::OK();
}
- auto session = OperationContextSession::get(opCtx);
- if (session && session->inMultiDocumentTransaction()) {
+ auto txnParticipant = TransactionParticipant::get(opCtx);
+ if (txnParticipant && txnParticipant->inMultiDocumentTransaction()) {
if (!_canAcceptNonLocalWrites && !getTestCommandsEnabled()) {
return Status(ErrorCodes::NotMaster,
"Multi-document transactions are only allowed on replica set primaries.");
diff --git a/src/mongo/db/s/coordinate_commit_transaction_command.cpp b/src/mongo/db/s/coordinate_commit_transaction_command.cpp
index e9f7669a54f..1e3b23a6cbc 100644
--- a/src/mongo/db/s/coordinate_commit_transaction_command.cpp
+++ b/src/mongo/db/s/coordinate_commit_transaction_command.cpp
@@ -33,7 +33,7 @@
#include "mongo/db/commands.h"
#include "mongo/db/repl/repl_client_info.h"
#include "mongo/db/s/coordinate_commit_transaction_gen.h"
-#include "mongo/db/session_catalog.h"
+#include "mongo/db/transaction_participant.h"
#include "mongo/util/log.h"
namespace mongo {
@@ -47,13 +47,13 @@ public:
using InvocationBase::InvocationBase;
void typedRun(OperationContext* opCtx) {
- auto session = OperationContextSession::get(opCtx);
+ auto txnParticipant = TransactionParticipant::get(opCtx);
uassert(ErrorCodes::CommandFailed,
- "commitTransaction must be run within a session",
- session);
+ "commitTransaction must be run within a transaction",
+ txnParticipant);
// commitTransaction is retryable.
- if (session->transactionIsCommitted()) {
+ if (txnParticipant->transactionIsCommitted()) {
// We set the client last op to the last optime observed by the system to ensure
// that we wait for the specified write concern on an optime greater than or equal
// to the commit oplog entry.
@@ -64,9 +64,9 @@ public:
uassert(ErrorCodes::NoSuchTransaction,
"Transaction isn't in progress",
- session->inMultiDocumentTransaction());
+ txnParticipant->inMultiDocumentTransaction());
- session->commitUnpreparedTransaction(opCtx);
+ txnParticipant->commitUnpreparedTransaction(opCtx);
}
private:
diff --git a/src/mongo/db/s/session_catalog_migration_destination.cpp b/src/mongo/db/s/session_catalog_migration_destination.cpp
index 68c62abbce2..2b12fee36e1 100644
--- a/src/mongo/db/s/session_catalog_migration_destination.cpp
+++ b/src/mongo/db/s/session_catalog_migration_destination.cpp
@@ -41,6 +41,7 @@
#include "mongo/db/repl/oplog_entry.h"
#include "mongo/db/s/migration_session_id.h"
#include "mongo/db/session_catalog.h"
+#include "mongo/db/transaction_participant.h"
#include "mongo/db/write_concern.h"
#include "mongo/s/client/shard_registry.h"
#include "mongo/s/grid.h"
@@ -246,11 +247,15 @@ ProcessOplogResult processSessionOplog(OperationContext* opCtx,
invariant(oplogEntry.getWallClockTime());
auto scopedSession = SessionCatalog::get(opCtx)->getOrCreateSession(opCtx, result.sessionId);
+ scopedSession->refreshFromStorageIfNeeded(opCtx);
if (!scopedSession->onMigrateBeginOnPrimary(opCtx, result.txnNum, stmtId)) {
// Don't continue migrating the transaction history
return lastResult;
}
+ auto txnParticipant = TransactionParticipant::getFromNonCheckedOutSession(scopedSession.get());
+ txnParticipant->checkForNewTxnNumber();
+
BSONObj object(result.isPrePostImage
? oplogEntry.getObject()
: BSON(SessionCatalogMigrationDestination::kSessionMigrateOplogTag << 1));
diff --git a/src/mongo/db/s/session_catalog_migration_destination_test.cpp b/src/mongo/db/s/session_catalog_migration_destination_test.cpp
index 5d8fa25978d..5299f54142f 100644
--- a/src/mongo/db/s/session_catalog_migration_destination_test.cpp
+++ b/src/mongo/db/s/session_catalog_migration_destination_test.cpp
@@ -36,6 +36,7 @@
#include "mongo/db/logical_session_cache_noop.h"
#include "mongo/db/logical_session_id.h"
#include "mongo/db/logical_session_id_gen.h"
+#include "mongo/db/operation_context_session_mongod.h"
#include "mongo/db/ops/write_ops_exec.h"
#include "mongo/db/ops/write_ops_gen.h"
#include "mongo/db/repl/oplog_entry.h"
@@ -171,7 +172,7 @@ public:
const LogicalSessionId& sessionId,
const TxnNumber& txnNum) {
auto scopedSession = SessionCatalog::get(opCtx)->getOrCreateSession(opCtx, sessionId);
- scopedSession->beginOrContinueTxnOnMigration(opCtx, txnNum);
+ scopedSession->beginOrContinueTxn(opCtx, txnNum);
return scopedSession;
}
@@ -243,8 +244,8 @@ public:
// up the session state and perform the insert.
initializeOperationSessionInfo(
innerOpCtx.get(), insertBuilder.obj(), true, true, true, false);
- OperationContextSession sessionTxnState(
- innerOpCtx.get(), true, boost::none, boost::none, "testDB", "insert");
+ OperationContextSessionMongod sessionTxnState(
+ innerOpCtx.get(), true, boost::none, boost::none);
const auto reply = performInserts(innerOpCtx.get(), insertRequest);
ASSERT(reply.results.size() == 1);
diff --git a/src/mongo/db/service_entry_point_common.cpp b/src/mongo/db/service_entry_point_common.cpp
index 00a7a4f6b6a..1249fd85b72 100644
--- a/src/mongo/db/service_entry_point_common.cpp
+++ b/src/mongo/db/service_entry_point_common.cpp
@@ -55,6 +55,7 @@
#include "mongo/db/logical_session_id.h"
#include "mongo/db/logical_session_id_helpers.h"
#include "mongo/db/logical_time_validator.h"
+#include "mongo/db/operation_context_session_mongod.h"
#include "mongo/db/ops/write_ops.h"
#include "mongo/db/ops/write_ops_exec.h"
#include "mongo/db/query/find.h"
@@ -71,10 +72,10 @@
#include "mongo/db/s/sharding_config_optime_gossip.h"
#include "mongo/db/s/sharding_state.h"
#include "mongo/db/service_entry_point_common.h"
-#include "mongo/db/session_catalog.h"
#include "mongo/db/snapshot_window_util.h"
#include "mongo/db/stats/counters.h"
#include "mongo/db/stats/top.h"
+#include "mongo/db/transaction_participant.h"
#include "mongo/rpc/factory.h"
#include "mongo/rpc/get_status_from_command_result.h"
#include "mongo/rpc/message.h"
@@ -464,15 +465,16 @@ void appendClusterAndOperationTime(OperationContext* opCtx,
void invokeInTransaction(OperationContext* opCtx,
CommandInvocation* invocation,
rpc::ReplyBuilderInterface* replyBuilder) {
- auto session = OperationContextSession::get(opCtx);
- if (!session) {
+ auto txnParticipant = TransactionParticipant::get(opCtx);
+ if (!txnParticipant) {
// Run the command directly if we're not in a transaction.
invocation->run(opCtx, replyBuilder);
return;
}
- session->unstashTransactionResources(opCtx, invocation->definition()->getName());
- ScopeGuard guard = MakeGuard([session, opCtx]() { session->abortActiveTransaction(opCtx); });
+ txnParticipant->unstashTransactionResources(opCtx, invocation->definition()->getName());
+ ScopeGuard guard =
+ MakeGuard([&txnParticipant, opCtx]() { txnParticipant->abortActiveTransaction(opCtx); });
invocation->run(opCtx, replyBuilder);
@@ -484,7 +486,7 @@ void invokeInTransaction(OperationContext* opCtx,
}
// Stash or commit the transaction when the command succeeds.
- session->stashTransactionResources(opCtx);
+ txnParticipant->stashTransactionResources(opCtx);
guard.Dismiss();
}
@@ -512,10 +514,11 @@ bool runCommandImpl(OperationContext* opCtx,
invokeInTransaction(opCtx, invocation, replyBuilder);
} else {
auto wcResult = uassertStatusOK(extractWriteConcern(opCtx, request.body));
- auto session = OperationContextSession::get(opCtx);
+ auto txnParticipant = TransactionParticipant::get(opCtx);
uassert(ErrorCodes::InvalidOptions,
"writeConcern is not allowed within a multi-statement transaction",
- wcResult.usedDefault || !session || !session->inMultiDocumentTransaction() ||
+ wcResult.usedDefault || !txnParticipant ||
+ !txnParticipant->inMultiDocumentTransaction() ||
invocation->definition()->getName() == "commitTransaction" ||
invocation->definition()->getName() == "abortTransaction" ||
invocation->definition()->getName() == "doTxn");
@@ -691,15 +694,15 @@ void execCommandDatabase(OperationContext* opCtx,
shouldCheckoutSession || !opCtx->getTxnNumber());
}
+ if (autocommitVal) {
+ uassertStatusOK(TransactionParticipant::isValid(dbname, command->getName()));
+ }
+
// This constructor will check out the session and start a transaction, if necessary. It
// handles the appropriate state management for both multi-statement transactions and
// retryable writes.
- OperationContextSession sessionTxnState(opCtx,
- shouldCheckoutSession,
- autocommitVal,
- startMultiDocTxn,
- dbname,
- command->getName());
+ OperationContextSessionMongod sessionTxnState(
+ opCtx, shouldCheckoutSession, autocommitVal, startMultiDocTxn);
std::unique_ptr<MaintenanceModeSetter> mmSetter;
@@ -808,11 +811,11 @@ void execCommandDatabase(OperationContext* opCtx,
}
auto& readConcernArgs = repl::ReadConcernArgs::get(opCtx);
- auto session = OperationContextSession::get(opCtx);
- if (!opCtx->getClient()->isInDirectClient() || !session ||
- !session->inMultiDocumentTransaction()) {
- const bool upconvertToSnapshot = session && session->inMultiDocumentTransaction() &&
- sessionOptions &&
+ auto txnParticipant = TransactionParticipant::get(opCtx);
+ if (!opCtx->getClient()->isInDirectClient() || !txnParticipant ||
+ !txnParticipant->inMultiDocumentTransaction()) {
+ const bool upconvertToSnapshot = txnParticipant &&
+ txnParticipant->inMultiDocumentTransaction() && sessionOptions &&
(sessionOptions->getStartTransaction() == boost::optional<bool>(true));
readConcernArgs = uassertStatusOK(
_extractReadConcern(invocation.get(), request.body, upconvertToSnapshot));
@@ -825,10 +828,9 @@ void execCommandDatabase(OperationContext* opCtx,
}
if (readConcernArgs.getLevel() == repl::ReadConcernLevel::kSnapshotReadConcern) {
- auto session = OperationContextSession::get(opCtx);
uassert(ErrorCodes::InvalidOptions,
"readConcern level snapshot is only valid in multi-statement transactions",
- session && session->inMultiDocumentTransaction());
+ txnParticipant && txnParticipant->inMultiDocumentTransaction());
uassert(ErrorCodes::InvalidOptions,
"readConcern level snapshot requires a session ID",
opCtx->getLogicalSessionId());
diff --git a/src/mongo/db/session.cpp b/src/mongo/db/session.cpp
index 530fbddcea6..217aa0bab62 100644
--- a/src/mongo/db/session.cpp
+++ b/src/mongo/db/session.cpp
@@ -33,7 +33,6 @@
#include "mongo/db/session.h"
#include "mongo/db/catalog/index_catalog.h"
-#include "mongo/db/commands/test_commands_enabled.h"
#include "mongo/db/concurrency/lock_state.h"
#include "mongo/db/concurrency/locker.h"
#include "mongo/db/concurrency/write_conflict_exception.h"
@@ -44,75 +43,18 @@
#include "mongo/db/operation_context.h"
#include "mongo/db/ops/update.h"
#include "mongo/db/query/get_executor.h"
-#include "mongo/db/repl/read_concern_args.h"
-#include "mongo/db/repl/repl_client_info.h"
#include "mongo/db/retryable_writes_stats.h"
-#include "mongo/db/server_parameters.h"
-#include "mongo/db/server_transactions_metrics.h"
-#include "mongo/db/stats/fill_locker_info.h"
#include "mongo/db/transaction_history_iterator.h"
#include "mongo/stdx/memory.h"
#include "mongo/transport/transport_layer.h"
#include "mongo/util/fail_point_service.h"
#include "mongo/util/log.h"
#include "mongo/util/mongoutils/str.h"
-#include "mongo/util/net/socket_utils.h"
namespace mongo {
-// Server parameter that dictates the max number of milliseconds that any transaction lock request
-// will wait for lock acquisition. If an operation provides a greater timeout in a lock request,
-// maxTransactionLockRequestTimeoutMillis will override it. If this is set to a negative value, it
-// is inactive and nothing will be overridden.
-//
-// 5 milliseconds will help avoid deadlocks, but will still allow fast-running metadata operations
-// to run without aborting transactions.
-MONGO_EXPORT_SERVER_PARAMETER(maxTransactionLockRequestTimeoutMillis, int, 5);
-
-// Server parameter that dictates the lifetime given to each transaction.
-// Transactions must eventually expire to preempt storage cache pressure immobilizing the system.
-MONGO_EXPORT_SERVER_PARAMETER(transactionLifetimeLimitSeconds, std::int32_t, 60)
- ->withValidator([](const auto& potentialNewValue) {
- if (potentialNewValue < 1) {
- return Status(ErrorCodes::BadValue,
- "transactionLifetimeLimitSeconds must be greater than or equal to 1s");
- }
-
- return Status::OK();
- });
-
-
namespace {
-// The command names that are allowed in a multi-document transaction.
-const StringMap<int> txnCmdWhitelist = {{"abortTransaction", 1},
- {"aggregate", 1},
- {"commitTransaction", 1},
- {"coordinateCommitTransaction", 1},
- {"delete", 1},
- {"distinct", 1},
- {"doTxn", 1},
- {"find", 1},
- {"findandmodify", 1},
- {"findAndModify", 1},
- {"geoSearch", 1},
- {"getMore", 1},
- {"insert", 1},
- {"killCursors", 1},
- {"prepareTransaction", 1},
- {"update", 1}};
-
-// The command names that are allowed in a multi-document transaction only when test commands are
-// enabled.
-const StringMap<int> txnCmdForTestingWhitelist = {{"dbHash", 1}};
-
-// The commands that can be run on the 'admin' database in multi-document transactions.
-const StringMap<int> txnAdminCommands = {{"abortTransaction", 1},
- {"commitTransaction", 1},
- {"coordinateCommitTransaction", 1},
- {"doTxn", 1},
- {"prepareTransaction", 1}};
-
void fassertOnRepeatedExecution(const LogicalSessionId& lsid,
TxnNumber txnNumber,
StmtId stmtId,
@@ -283,11 +225,6 @@ void updateSessionEntry(OperationContext* opCtx, const UpdateRequest& updateRequ
// will be allowed to commit.
MONGO_FAIL_POINT_DEFINE(onPrimaryTransactionalWrite);
-// Failpoint which will pause an operation just after allocating a point-in-time storage engine
-// transaction.
-MONGO_FAIL_POINT_DEFINE(hangAfterPreallocateSnapshot);
-
-MONGO_FAIL_POINT_DEFINE(hangAfterReservingPrepareTimestamp);
} // namespace
const BSONObj Session::kDeadEndSentinel(BSON("$incompleteOplogHistory" << 1));
@@ -320,18 +257,18 @@ void Session::refreshFromStorageIfNeeded(OperationContext* opCtx) {
_lastWrittenSessionRecord = std::move(activeTxnHistory.lastTxnRecord);
if (_lastWrittenSessionRecord) {
+ if (!_lastRefreshState) {
+ _lastRefreshState.emplace();
+ }
+
+ _lastRefreshState->refreshCount++;
+
_activeTxnNumber = _lastWrittenSessionRecord->getTxnNum();
+ _lastRefreshState->txnNumber = _activeTxnNumber;
+
_activeTxnCommittedStatements = std::move(activeTxnHistory.committedStatements);
_hasIncompleteHistory = activeTxnHistory.hasIncompleteHistory;
- if (activeTxnHistory.transactionCommitted) {
- // When refreshing the state from storage, we relax transition validation since
- // all states are valid next states and we do not want to pollute the state
- // transition table for other callers.
- _txnState.transitionTo(
- ul,
- TransactionState::kCommitted,
- TransactionState::TransitionValidation::kRelaxTransitionValidation);
- }
+ _lastRefreshState->isCommitted = activeTxnHistory.transactionCommitted;
}
break;
@@ -339,59 +276,9 @@ void Session::refreshFromStorageIfNeeded(OperationContext* opCtx) {
}
}
-void Session::beginOrContinueTxn(OperationContext* opCtx,
- TxnNumber txnNumber,
- boost::optional<bool> autocommit,
- boost::optional<bool> startTransaction,
- StringData dbName,
- StringData cmdName) {
- if (opCtx->getClient()->isInDirectClient()) {
- return;
- }
-
- invariant(!opCtx->lockState()->isLocked());
-
- uassert(ErrorCodes::OperationNotSupportedInTransaction,
- "Cannot run 'count' in a multi-document transaction. Please see "
- "http://dochub.mongodb.org/core/transaction-count for a recommended alternative.",
- !autocommit || cmdName != "count"_sd);
-
- uassert(ErrorCodes::OperationNotSupportedInTransaction,
- str::stream() << "Cannot run '" << cmdName << "' in a multi-document transaction.",
- !autocommit || txnCmdWhitelist.find(cmdName) != txnCmdWhitelist.cend() ||
- (getTestCommandsEnabled() &&
- txnCmdForTestingWhitelist.find(cmdName) != txnCmdForTestingWhitelist.cend()));
-
- uassert(ErrorCodes::OperationNotSupportedInTransaction,
- str::stream() << "Cannot run command against the '" << dbName
- << "' database in a transaction",
- !autocommit || (dbName != "config"_sd && dbName != "local"_sd &&
- (dbName != "admin"_sd ||
- txnAdminCommands.find(cmdName) != txnAdminCommands.cend())));
-
- stdx::lock_guard<stdx::mutex> lg(_mutex);
- _beginOrContinueTxn(lg, txnNumber, autocommit, startTransaction);
-}
-
-void Session::beginOrContinueTxnOnMigration(OperationContext* opCtx, TxnNumber txnNumber) {
- invariant(!opCtx->getClient()->isInDirectClient());
- invariant(!opCtx->lockState()->isLocked());
-
+void Session::beginOrContinueTxn(OperationContext* opCtx, TxnNumber txnNumber) {
stdx::lock_guard<stdx::mutex> lg(_mutex);
- _beginOrContinueTxnOnMigration(lg, txnNumber);
-}
-
-void Session::setSpeculativeTransactionOpTimeToLastApplied(OperationContext* opCtx) {
- stdx::lock_guard<stdx::mutex> lg(_mutex);
- repl::ReplicationCoordinator* replCoord =
- repl::ReplicationCoordinator::get(opCtx->getClient()->getServiceContext());
- opCtx->recoveryUnit()->setTimestampReadSource(RecoveryUnit::ReadSource::kLastAppliedSnapshot);
- opCtx->recoveryUnit()->preallocateSnapshot();
- auto readTimestamp = opCtx->recoveryUnit()->getPointInTimeReadTimestamp();
- invariant(readTimestamp);
- // Transactions do not survive term changes, so combining "getTerm" here with the
- // recovery unit timestamp does not cause races.
- _speculativeTransactionReadOpTime = {*readTimestamp, replCoord->getTerm()};
+ _beginOrContinueTxn(lg, txnNumber);
}
void Session::onWriteOpCompletedOnPrimary(OperationContext* opCtx,
@@ -425,7 +312,7 @@ void Session::onWriteOpCompletedOnPrimary(OperationContext* opCtx,
}
bool Session::onMigrateBeginOnPrimary(OperationContext* opCtx, TxnNumber txnNumber, StmtId stmtId) {
- beginOrContinueTxnOnMigration(opCtx, txnNumber);
+ beginOrContinueTxn(opCtx, txnNumber);
try {
if (checkStatementExecuted(opCtx, txnNumber, stmtId)) {
@@ -457,7 +344,7 @@ void Session::onMigrateCompletedOnPrimary(OperationContext* opCtx,
stdx::unique_lock<stdx::mutex> ul(_mutex);
_checkValid(ul);
- _checkIsActiveTransaction(ul, txnNumber, false);
+ _checkIsActiveTransaction(ul, txnNumber);
// If the transaction has a populated lastWriteDate, we will use that as the most up-to-date
// value. Using the lastWriteDate from the oplog being migrated may move the lastWriteDate
@@ -481,6 +368,14 @@ void Session::onMigrateCompletedOnPrimary(OperationContext* opCtx,
void Session::invalidate() {
stdx::lock_guard<stdx::mutex> lg(_mutex);
+
+ if (_isTxnNumberLocked) {
+ invariant(_txnNumberLockConflictStatus);
+ uasserted(50908,
+ str::stream() << "cannot invalidate session because txnNumber is locked: "
+ << *_txnNumberLockConflictStatus);
+ }
+
_isValid = false;
_numInvalidations++;
@@ -488,14 +383,13 @@ void Session::invalidate() {
_activeTxnNumber = kUninitializedTxnNumber;
_activeTxnCommittedStatements.clear();
- _speculativeTransactionReadOpTime = repl::OpTime();
_hasIncompleteHistory = false;
}
repl::OpTime Session::getLastWriteOpTime(TxnNumber txnNumber) const {
stdx::lock_guard<stdx::mutex> lg(_mutex);
_checkValid(lg);
- _checkIsActiveTransaction(lg, txnNumber, false);
+ _checkIsActiveTransaction(lg, txnNumber);
if (!_lastWrittenSessionRecord || _lastWrittenSessionRecord->getTxnNum() != txnNumber)
return {};
@@ -530,10 +424,7 @@ bool Session::checkStatementExecutedNoOplogEntryFetch(TxnNumber txnNumber, StmtI
return bool(_checkStatementExecuted(lg, txnNumber, stmtId));
}
-void Session::_beginOrContinueTxn(WithLock wl,
- TxnNumber txnNumber,
- boost::optional<bool> autocommit,
- boost::optional<bool> startTransaction) {
+void Session::_beginOrContinueTxn(WithLock wl, TxnNumber txnNumber) {
// Check whether the session information needs to be refreshed from disk.
_checkValid(wl);
@@ -546,86 +437,11 @@ void Session::_beginOrContinueTxn(WithLock wl,
// Continue an active transaction.
//
if (txnNumber == _activeTxnNumber) {
-
- // It is never valid to specify 'startTransaction' on an active transaction.
- uassert(ErrorCodes::ConflictingOperationInProgress,
- str::stream() << "Cannot specify 'startTransaction' on transaction " << txnNumber
- << " since it is already in progress.",
- startTransaction == boost::none);
-
- // Continue a retryable write.
- if (_txnState.isNone(wl)) {
- uassert(ErrorCodes::InvalidOptions,
- "Cannot specify 'autocommit' on an operation not inside a multi-statement "
- "transaction.",
- autocommit == boost::none);
- return;
- }
-
- // Continue a multi-statement transaction. In this case, it is required that
- // autocommit=false be given as an argument on the request. Retryable writes will have
- // _autocommit=true, so that is why we verify that _autocommit=false here.
- if (!_autocommit) {
- uassert(
- ErrorCodes::InvalidOptions,
- "Must specify autocommit=false on all operations of a multi-statement transaction.",
- autocommit == boost::optional<bool>(false));
- if (_txnState.isInProgress(wl) && !_txnResourceStash) {
- // This indicates that the first command in the transaction failed but did not
- // implicitly abort the transaction. It is not safe to continue the transaction, in
- // particular because we have not saved the readConcern from the first statement of
- // the transaction.
- _abortTransactionOnSession(wl);
- uasserted(ErrorCodes::NoSuchTransaction,
- str::stream() << "Transaction " << txnNumber << " has been aborted.");
- }
- }
return;
}
- //
- // Start a new transaction.
- //
- // At this point, the given transaction number must be > _activeTxnNumber. Existence of an
- // 'autocommit' field means we interpret this operation as part of a multi-document transaction.
invariant(txnNumber > _activeTxnNumber);
- if (autocommit) {
- // Start a multi-document transaction.
- invariant(*autocommit == false);
- uassert(ErrorCodes::NoSuchTransaction,
- str::stream() << "Given transaction number " << txnNumber
- << " does not match any in-progress transactions.",
- startTransaction != boost::none);
-
- // We cannot start a transaction if a prepared transaction is already running on the
- // session.
- uassert(ErrorCodes::PreparedTransactionInProgress,
- "Cannot start a new transaction when a prepared transaction already exists on the "
- "session.",
- !_txnState.isPrepared(wl));
-
- _setActiveTxn(wl, txnNumber);
- _autocommit = false;
- _txnState.transitionTo(wl, TransactionState::kInProgress);
- // Tracks various transactions metrics.
- _singleTransactionStats = SingleTransactionStats();
- _singleTransactionStats->setStartTime(curTimeMicros64());
- _transactionExpireDate =
- Date_t::fromMillisSinceEpoch(_singleTransactionStats->getStartTime() / 1000) +
- stdx::chrono::seconds{transactionLifetimeLimitSeconds.load()};
- ServerTransactionsMetrics::get(getGlobalServiceContext())->incrementTotalStarted();
- ServerTransactionsMetrics::get(getGlobalServiceContext())->incrementCurrentOpen();
- } else {
- // Execute a retryable write.
- invariant(startTransaction == boost::none);
- _setActiveTxn(wl, txnNumber);
- _autocommit = true;
- _txnState.transitionTo(wl, TransactionState::kNone);
- // SingleTransactionStats are only for multi-document transactions.
- _singleTransactionStats = boost::none;
- }
-
- invariant(_transactionOperations.empty());
+ _setActiveTxn(wl, txnNumber);
}
void Session::_checkTxnValid(WithLock, TxnNumber txnNumber) const {
@@ -638,750 +454,15 @@ void Session::_checkTxnValid(WithLock, TxnNumber txnNumber) const {
txnNumber >= _activeTxnNumber);
}
-Session::OplogSlotReserver::OplogSlotReserver(OperationContext* opCtx) {
- // Stash the transaction on the OperationContext on the stack. At the end of this function it
- // will be unstashed onto the OperationContext.
- Session::SideTransactionBlock sideTxn(opCtx);
-
- // Begin a new WUOW and reserve a slot in the oplog.
- WriteUnitOfWork wuow(opCtx);
- _oplogSlot = repl::getNextOpTime(opCtx);
-
- // Release the WUOW state since this WUOW is no longer in use.
- wuow.release();
-
- // The new transaction should have an empty locker, and thus we do not need to save it.
- invariant(opCtx->lockState()->getClientState() == Locker::ClientState::kInactive);
- _locker = opCtx->swapLockState(stdx::make_unique<LockerImpl>());
- _locker->unsetThreadId();
-
- // This thread must still respect the transaction lock timeout, since it can prevent the
- // transaction from making progress.
- auto maxTransactionLockMillis = maxTransactionLockRequestTimeoutMillis.load();
- if (maxTransactionLockMillis >= 0) {
- opCtx->lockState()->setMaxLockTimeout(Milliseconds(maxTransactionLockMillis));
- }
-
- // Save the RecoveryUnit from the new transaction and replace it with an empty one.
- _recoveryUnit = std::unique_ptr<RecoveryUnit>(opCtx->releaseRecoveryUnit());
- opCtx->setRecoveryUnit(opCtx->getServiceContext()->getStorageEngine()->newRecoveryUnit(),
- WriteUnitOfWork::RecoveryUnitState::kNotInUnitOfWork);
-}
-
-Session::OplogSlotReserver::~OplogSlotReserver() {
- // If the constructor did not complete, we do not attempt to abort the units of work.
- if (_recoveryUnit) {
- // We should be at WUOW nesting level 1, only the top level WUOW for the oplog reservation
- // side transaction.
- _locker->endWriteUnitOfWork();
- invariant(!_locker->inAWriteUnitOfWork());
- _recoveryUnit->abortUnitOfWork();
- }
-}
-
-Session::TxnResources::TxnResources(OperationContext* opCtx, bool keepTicket) {
- _ruState = opCtx->getWriteUnitOfWork()->release();
- opCtx->setWriteUnitOfWork(nullptr);
-
- _locker = opCtx->swapLockState(stdx::make_unique<LockerImpl>());
- if (!keepTicket) {
- _locker->releaseTicket();
- }
- _locker->unsetThreadId();
-
- // This thread must still respect the transaction lock timeout, since it can prevent the
- // transaction from making progress.
- auto maxTransactionLockMillis = maxTransactionLockRequestTimeoutMillis.load();
- if (maxTransactionLockMillis >= 0) {
- opCtx->lockState()->setMaxLockTimeout(Milliseconds(maxTransactionLockMillis));
- }
-
- _recoveryUnit = std::unique_ptr<RecoveryUnit>(opCtx->releaseRecoveryUnit());
- opCtx->setRecoveryUnit(opCtx->getServiceContext()->getStorageEngine()->newRecoveryUnit(),
- WriteUnitOfWork::RecoveryUnitState::kNotInUnitOfWork);
-
- _readConcernArgs = repl::ReadConcernArgs::get(opCtx);
-}
-
-Session::TxnResources::~TxnResources() {
- if (!_released && _recoveryUnit) {
- // This should only be reached when aborting a transaction that isn't active, i.e.
- // when starting a new transaction before completing an old one. So we should
- // be at WUOW nesting level 1 (only the top level WriteUnitOfWork).
- _locker->endWriteUnitOfWork();
- invariant(!_locker->inAWriteUnitOfWork());
- _recoveryUnit->abortUnitOfWork();
- }
-}
-
-void Session::TxnResources::release(OperationContext* opCtx) {
- // Perform operations that can fail the release before marking the TxnResources as released.
- _locker->reacquireTicket(opCtx);
-
- invariant(!_released);
- _released = true;
-
- // We intentionally do not capture the return value of swapLockState(), which is just an empty
- // locker. At the end of the operation, if the transaction is not complete, we will stash the
- // operation context's locker and replace it with a new empty locker.
- invariant(opCtx->lockState()->getClientState() == Locker::ClientState::kInactive);
- opCtx->swapLockState(std::move(_locker));
- opCtx->lockState()->updateThreadIdToCurrentThread();
-
- auto oldState = opCtx->setRecoveryUnit(_recoveryUnit.release(),
- WriteUnitOfWork::RecoveryUnitState::kNotInUnitOfWork);
- invariant(oldState == WriteUnitOfWork::RecoveryUnitState::kNotInUnitOfWork,
- str::stream() << "RecoveryUnit state was " << oldState);
-
- opCtx->setWriteUnitOfWork(WriteUnitOfWork::createForSnapshotResume(opCtx, _ruState));
-
- auto& readConcernArgs = repl::ReadConcernArgs::get(opCtx);
- readConcernArgs = _readConcernArgs;
-}
-
-Session::SideTransactionBlock::SideTransactionBlock(OperationContext* opCtx) : _opCtx(opCtx) {
- if (_opCtx->getWriteUnitOfWork()) {
- // This must be done under the client lock, since we are modifying '_opCtx'.
- stdx::lock_guard<Client> clientLock(*_opCtx->getClient());
- _txnResources = Session::TxnResources(_opCtx, true /* keepTicket*/);
- }
-}
-
-Session::SideTransactionBlock::~SideTransactionBlock() {
- if (_txnResources) {
- // Restore the transaction state onto '_opCtx'. This must be done under the
- // client lock, since we are modifying '_opCtx'.
- stdx::lock_guard<Client> clientLock(*_opCtx->getClient());
- _txnResources->release(_opCtx);
- }
-}
-
-void Session::stashTransactionResources(OperationContext* opCtx) {
- if (opCtx->getClient()->isInDirectClient()) {
- return;
- }
-
- invariant(opCtx->getTxnNumber());
-
- // We must lock the Client to change the Locker on the OperationContext and the Session mutex to
- // access Session state. We must lock the Client before the Session mutex, since the Client
- // effectively owns the Session. That is, a user might lock the Client to ensure it doesn't go
- // away, and then lock the Session owned by that client. We rely on the fact that we are not
- // using the DefaultLockerImpl to avoid deadlock.
- stdx::lock_guard<Client> lk(*opCtx->getClient());
- stdx::unique_lock<stdx::mutex> lg(_mutex);
-
- // Always check '_activeTxnNumber', since it can be modified by migration, which does not
- // check out the session. We intentionally do not error if _txnState=kAborted, since we
- // expect this function to be called at the end of the 'abortTransaction' command.
- _checkIsActiveTransaction(lg, *opCtx->getTxnNumber(), false);
-
- if (!_txnState.inMultiDocumentTransaction(lg)) {
- // Not in a multi-document transaction: nothing to do.
- return;
- }
-
- if (_singleTransactionStats->isActive()) {
- _singleTransactionStats->setInactive(curTimeMicros64());
- }
-
- // Add the latest operation stats to the aggregate OpDebug object stored in the
- // SingleTransactionStats instance on the Session.
- _singleTransactionStats->getOpDebug()->additiveMetrics.add(
- CurOp::get(opCtx)->debug().additiveMetrics);
-
- invariant(!_txnResourceStash);
- _txnResourceStash = TxnResources(opCtx);
-
- // We accept possible slight inaccuracies in these counters from non-atomicity.
- ServerTransactionsMetrics::get(opCtx)->decrementCurrentActive();
- ServerTransactionsMetrics::get(opCtx)->incrementCurrentInactive();
-
- // Update the LastClientInfo object stored in the SingleTransactionStats instance on the Session
- // with this Client's information. This is the last client that ran a transaction operation on
- // the Session.
- _singleTransactionStats->updateLastClientInfo(opCtx->getClient());
-}
-
-void Session::unstashTransactionResources(OperationContext* opCtx, const std::string& cmdName) {
- if (opCtx->getClient()->isInDirectClient()) {
- return;
- }
-
- invariant(opCtx->getTxnNumber());
-
- {
- // We must lock the Client to change the Locker on the OperationContext and the Session
- // mutex to access Session state. We must lock the Client before the Session mutex, since
- // the Client effectively owns the Session. That is, a user might lock the Client to ensure
- // it doesn't go away, and then lock the Session owned by that client.
- stdx::lock_guard<Client> lk(*opCtx->getClient());
- stdx::lock_guard<stdx::mutex> lg(_mutex);
-
- // Always check '_activeTxnNumber' and '_txnState', since they can be modified by session
- // kill and migration, which do not check out the session.
- _checkIsActiveTransaction(lg, *opCtx->getTxnNumber(), false);
-
- // If this is not a multi-document transaction, there is nothing to unstash.
- if (_txnState.isNone(lg)) {
- invariant(!_txnResourceStash);
- return;
- }
-
- // Throw NoSuchTransaction error instead of TransactionAborted error since this is the entry
- // point of transaction execution.
- uassert(ErrorCodes::NoSuchTransaction,
- str::stream() << "Transaction " << *opCtx->getTxnNumber() << " has been aborted.",
- !_txnState.isAborted(lg));
-
- // Cannot change committed transaction but allow retrying commitTransaction command.
- uassert(ErrorCodes::TransactionCommitted,
- str::stream() << "Transaction " << *opCtx->getTxnNumber() << " has been committed.",
- cmdName == "commitTransaction" || !_txnState.isCommitted(lg));
-
- if (_txnResourceStash) {
- // Transaction resources already exist for this transaction. Transfer them from the
- // stash to the operation context.
- auto& readConcernArgs = repl::ReadConcernArgs::get(opCtx);
- uassert(ErrorCodes::InvalidOptions,
- "Only the first command in a transaction may specify a readConcern",
- readConcernArgs.isEmpty());
- _txnResourceStash->release(opCtx);
- _txnResourceStash = boost::none;
- // Set the starting active time for this transaction.
- if (_txnState.isInProgress(lk)) {
- _singleTransactionStats->setActive(curTimeMicros64());
- }
- // We accept possible slight inaccuracies in these counters from non-atomicity.
- ServerTransactionsMetrics::get(opCtx)->incrementCurrentActive();
- ServerTransactionsMetrics::get(opCtx)->decrementCurrentInactive();
- return;
- }
-
- // If we have no transaction resources then we cannot be prepared. If we're not in progress,
- // we don't do anything else.
- invariant(!_txnState.isPrepared(lk));
- if (!_txnState.isInProgress(lg)) {
- // At this point we're either committed and this is a 'commitTransaction' command, or we
- // are in the process of committing.
- return;
- }
-
- // Stashed transaction resources do not exist for this in-progress multi-document
- // transaction. Set up the transaction resources on the opCtx.
- opCtx->setWriteUnitOfWork(std::make_unique<WriteUnitOfWork>(opCtx));
- ServerTransactionsMetrics::get(getGlobalServiceContext())->incrementCurrentActive();
-
- // Set the starting active time for this transaction.
- _singleTransactionStats->setActive(curTimeMicros64());
-
- // If maxTransactionLockRequestTimeoutMillis is set, then we will ensure no
- // future lock request waits longer than maxTransactionLockRequestTimeoutMillis
- // to acquire a lock. This is to avoid deadlocks and minimize non-transaction
- // operation performance degradations.
- auto maxTransactionLockMillis = maxTransactionLockRequestTimeoutMillis.load();
- if (maxTransactionLockMillis >= 0) {
- opCtx->lockState()->setMaxLockTimeout(Milliseconds(maxTransactionLockMillis));
- }
- }
-
- // Storage engine transactions may be started in a lazy manner. By explicitly
- // starting here we ensure that a point-in-time snapshot is established during the
- // first operation of a transaction.
- //
- // Active transactions are protected by the locking subsystem, so we must always hold at least a
- // Global intent lock before starting a transaction. We pessimistically acquire an intent
- // exclusive lock here because we might be doing writes in this transaction, and it is currently
- // not deadlock-safe to upgrade IS to IX.
- Lock::GlobalLock(opCtx, MODE_IX);
- opCtx->recoveryUnit()->preallocateSnapshot();
-
- // The Client lock must not be held when executing this failpoint as it will block currentOp
- // execution.
- MONGO_FAIL_POINT_PAUSE_WHILE_SET(hangAfterPreallocateSnapshot);
-}
-
-Timestamp Session::prepareTransaction(OperationContext* opCtx) {
- // This ScopeGuard is created outside of the lock so that the lock is always released before
- // this is called.
- ScopeGuard abortGuard = MakeGuard([&] { abortActiveTransaction(opCtx); });
-
- stdx::unique_lock<stdx::mutex> lk(_mutex);
- // Always check '_activeTxnNumber' and '_txnState', since they can be modified by
- // session kill and migration, which do not check out the session.
- _checkIsActiveTransaction(lk, *opCtx->getTxnNumber(), true);
-
- uassert(ErrorCodes::TransactionCommitted,
- str::stream() << "Transaction " << *opCtx->getTxnNumber() << " has been committed.",
- !_txnState.isCommitted(lk));
-
- _txnState.transitionTo(lk, TransactionState::kPrepared);
-
- // Reserve an optime for the 'prepareTimestamp'. This will create a hole in the oplog and cause
- // 'snapshot' and 'afterClusterTime' readers to block until this transaction is done being
- // prepared. When the OplogSlotReserver goes out of scope and is destroyed, the
- // storage-transaction it uses to keep the hole open will abort and the slot (and corresponding
- // oplog hole) will vanish.
- OplogSlotReserver oplogSlotReserver(opCtx);
- const auto prepareOplogSlot = oplogSlotReserver.getReservedOplogSlot();
- const auto prepareTimestamp = prepareOplogSlot.opTime.getTimestamp();
-
- if (MONGO_FAIL_POINT(hangAfterReservingPrepareTimestamp)) {
- // This log output is used in js tests so please leave it.
- log() << "transaction - hangAfterReservingPrepareTimestamp fail point "
- "enabled. Blocking until fail point is disabled. Prepare OpTime: "
- << prepareOplogSlot.opTime;
- MONGO_FAIL_POINT_PAUSE_WHILE_SET(hangAfterReservingPrepareTimestamp);
- }
-
- opCtx->recoveryUnit()->setPrepareTimestamp(prepareTimestamp);
- opCtx->getWriteUnitOfWork()->prepare();
-
- // We need to unlock the session to run the opObserver onTransactionPrepare, which calls back
- // into the session.
- lk.unlock();
- auto opObserver = opCtx->getServiceContext()->getOpObserver();
- invariant(opObserver);
- opObserver->onTransactionPrepare(opCtx, prepareOplogSlot);
-
- // After the oplog entry is written successfully, it is illegal to implicitly abort or fail.
- try {
- abortGuard.Dismiss();
-
- lk.lock();
-
- // Although we are not allowed to abort here, we check that we don't even try to. If we do
- // try to, that is a bug and we will fassert below.
- _checkIsActiveTransaction(lk, *opCtx->getTxnNumber(), true);
-
- // Ensure that the transaction is still prepared.
- invariant(_txnState.isPrepared(lk), str::stream() << "Current state: " << _txnState);
- } catch (...) {
- severe() << "Illegal exception after transaction was prepared.";
- fassertFailedWithStatus(50906, exceptionToStatus());
- }
- return prepareTimestamp;
-}
-
-void Session::abortArbitraryTransaction() {
- stdx::lock_guard<stdx::mutex> lock(_mutex);
- _abortArbitraryTransaction(lock);
-}
-
-void Session::abortArbitraryTransactionIfExpired() {
- stdx::lock_guard<stdx::mutex> lock(_mutex);
- if (!_transactionExpireDate || _transactionExpireDate >= Date_t::now()) {
- return;
- }
- _abortArbitraryTransaction(lock);
-}
-
-void Session::_abortArbitraryTransaction(WithLock lock) {
- if (!_txnState.isInProgress(lock)) {
- // We do not want to abort transactions that are prepared unless we get an
- // 'abortTransaction' command.
- return;
- }
-
- _abortTransactionOnSession(lock);
-}
-
-void Session::abortActiveTransaction(OperationContext* opCtx) {
- _abortActiveTransaction(opCtx, TransactionState::kInProgress | TransactionState::kPrepared);
-}
-
-void Session::_abortActiveTransaction(OperationContext* opCtx,
- TransactionState::StateSet expectedStates) {
- stdx::lock_guard<stdx::mutex> lock(_mutex);
-
- invariant(!_txnResourceStash);
-
- if (!_txnState.isNone(lock)) {
- // Add the latest operation stats to the aggregate OpDebug object stored in the
- // SingleTransactionStats instance on the Session.
- _singleTransactionStats->getOpDebug()->additiveMetrics.add(
- CurOp::get(opCtx)->debug().additiveMetrics);
-
- // Update the LastClientInfo object stored in the SingleTransactionStats instance on the
- // Session with this Client's information.
- _singleTransactionStats->updateLastClientInfo(opCtx->getClient());
- }
-
- // Only abort the transaction in session if it's in expected states.
- // When the state of active transaction on session is not expected, it means another
- // thread has already aborted the transaction on session.
- if (_txnState.isInSet(lock, expectedStates)) {
- invariant(opCtx->getTxnNumber() == _activeTxnNumber);
- _abortTransactionOnSession(lock);
- }
-
- // Log the transaction if its duration is longer than the slowMS command threshold.
- _logSlowTransaction(lock,
- &(opCtx->lockState()->getLockerInfo())->stats,
- TransactionState::kAborted,
- repl::ReadConcernArgs::get(opCtx));
-
- // Clean up the transaction resources on opCtx even if the transaction on session has been
- // aborted.
- _cleanUpTxnResourceOnOpCtx(opCtx);
-}
-
-void Session::_abortTransactionOnSession(WithLock wl) {
- if (!_txnState.isNone(wl)) {
- _singleTransactionStats->setEndTime(curTimeMicros64());
- // The transaction has aborted, so we mark it as inactive.
- if (_singleTransactionStats->isActive()) {
- _singleTransactionStats->setInactive(curTimeMicros64());
- }
- }
-
- if (_txnResourceStash) {
- // The transaction is stashed, so we abort the inactive transaction on session.
- _logSlowTransaction(wl,
- &(_txnResourceStash->locker()->getLockerInfo())->stats,
- TransactionState::kAborted,
- _txnResourceStash->getReadConcernArgs());
- _txnResourceStash = boost::none;
- ServerTransactionsMetrics::get(getGlobalServiceContext())->decrementCurrentInactive();
- } else {
- // Transaction resource has been unstashed and transferred into an active opCtx, which will
- // clean it up.
- ServerTransactionsMetrics::get(getGlobalServiceContext())->decrementCurrentActive();
- }
-
- _transactionOperationBytes = 0;
- _transactionOperations.clear();
- _txnState.transitionTo(wl, TransactionState::kAborted);
- _speculativeTransactionReadOpTime = repl::OpTime();
- ServerTransactionsMetrics::get(getGlobalServiceContext())->incrementTotalAborted();
- ServerTransactionsMetrics::get(getGlobalServiceContext())->decrementCurrentOpen();
-}
-
-void Session::_cleanUpTxnResourceOnOpCtx(OperationContext* opCtx) {
- // Reset the WUOW. We should be able to abort empty transactions that don't have WUOW.
- if (opCtx->getWriteUnitOfWork()) {
- opCtx->setWriteUnitOfWork(nullptr);
- }
- // We must clear the recovery unit and locker so any post-transaction writes can run without
- // transactional settings such as a read timestamp.
- opCtx->setRecoveryUnit(opCtx->getServiceContext()->getStorageEngine()->newRecoveryUnit(),
- WriteUnitOfWork::RecoveryUnitState::kNotInUnitOfWork);
- opCtx->lockState()->unsetMaxLockTimeout();
-}
-
-void Session::_beginOrContinueTxnOnMigration(WithLock wl, TxnNumber txnNumber) {
- _checkValid(wl);
- _checkTxnValid(wl, txnNumber);
-
- // Check for continuing an existing transaction
- if (txnNumber == _activeTxnNumber)
- return;
-
- _setActiveTxn(wl, txnNumber);
-}
-
void Session::_setActiveTxn(WithLock wl, TxnNumber txnNumber) {
- // Abort the existing transaction if it's not prepared, committed, or aborted.
- if (_txnState.isInProgress(wl)) {
- _abortTransactionOnSession(wl);
+ if (_isTxnNumberLocked) {
+ invariant(_txnNumberLockConflictStatus);
+ uassertStatusOK(*_txnNumberLockConflictStatus);
}
+
_activeTxnNumber = txnNumber;
_activeTxnCommittedStatements.clear();
_hasIncompleteHistory = false;
- _txnState.transitionTo(wl, TransactionState::kNone);
- _singleTransactionStats = boost::none;
- _speculativeTransactionReadOpTime = repl::OpTime();
- _multikeyPathInfo.clear();
-}
-
-void Session::addTransactionOperation(OperationContext* opCtx,
- const repl::ReplOperation& operation) {
- stdx::lock_guard<stdx::mutex> lk(_mutex);
-
- // Always check '_activeTxnNumber' and '_txnState', since they can be modified by session kill
- // and migration, which do not check out the session.
- _checkIsActiveTransaction(lk, *opCtx->getTxnNumber(), true);
-
- // Ensure that we only ever add operations to an in progress transaction.
- invariant(_txnState.isInProgress(lk), str::stream() << "Current state: " << _txnState);
-
- invariant(!_autocommit && _activeTxnNumber != kUninitializedTxnNumber);
- invariant(opCtx->lockState()->inAWriteUnitOfWork());
- _transactionOperations.push_back(operation);
- _transactionOperationBytes += repl::OplogEntry::getReplOperationSize(operation);
- // _transactionOperationBytes is based on the in-memory size of the operation. With overhead,
- // we expect the BSON size of the operation to be larger, so it's possible to make a transaction
- // just a bit too large and have it fail only in the commit. It's still useful to fail early
- // when possible (e.g. to avoid exhausting server memory).
- uassert(ErrorCodes::TransactionTooLarge,
- str::stream() << "Total size of all transaction operations must be less than "
- << BSONObjMaxInternalSize
- << ". Actual size is "
- << _transactionOperationBytes,
- _transactionOperationBytes <= BSONObjMaxInternalSize);
-}
-
-std::vector<repl::ReplOperation> Session::endTransactionAndRetrieveOperations(
- OperationContext* opCtx) {
- stdx::lock_guard<stdx::mutex> lk(_mutex);
-
- // Always check '_activeTxnNumber' and '_txnState', since they can be modified by session kill
- // and migration, which do not check out the session.
- _checkIsActiveTransaction(lk, *opCtx->getTxnNumber(), true);
-
- // Ensure that we only ever end a transaction when prepared or committing.
- invariant(_txnState.isPrepared(lk) || _txnState.isCommittingWithoutPrepare(lk),
- str::stream() << "Current state: " << _txnState);
-
- invariant(!_autocommit);
- _transactionOperationBytes = 0;
- return std::move(_transactionOperations);
-}
-
-void Session::commitUnpreparedTransaction(OperationContext* opCtx) {
- stdx::unique_lock<stdx::mutex> lk(_mutex);
- uassert(ErrorCodes::InvalidOptions,
- "commitTransaction must provide commitTimestamp to prepared transaction.",
- !_txnState.isPrepared(lk));
-
- // Always check '_activeTxnNumber' and '_txnState', since they can be modified by session kill
- // and migration, which do not check out the session.
- _checkIsActiveTransaction(lk, *opCtx->getTxnNumber(), true);
-
- _txnState.transitionTo(lk, TransactionState::kCommittingWithoutPrepare);
-
- // We need to unlock the session to run the opObserver onTransactionCommit, which calls back
- // into the session.
- lk.unlock();
- auto opObserver = opCtx->getServiceContext()->getOpObserver();
- invariant(opObserver);
- opObserver->onTransactionCommit(opCtx, false /* wasPrepared */);
- lk.lock();
-
- _checkIsActiveTransaction(lk, *opCtx->getTxnNumber(), true);
- _commitTransaction(std::move(lk), opCtx);
-}
-
-void Session::commitPreparedTransaction(OperationContext* opCtx, Timestamp commitTimestamp) {
- stdx::unique_lock<stdx::mutex> lk(_mutex);
- uassert(ErrorCodes::InvalidOptions,
- "commitTransaction cannot provide commitTimestamp to unprepared transaction.",
- _txnState.isPrepared(lk));
- uassert(
- ErrorCodes::InvalidOptions, "'commitTimestamp' cannot be null", !commitTimestamp.isNull());
-
- // Always check '_activeTxnNumber' and '_txnState', since they can be modified by session kill
- // and migration, which do not check out the session.
- _checkIsActiveTransaction(lk, *opCtx->getTxnNumber(), true);
-
- _txnState.transitionTo(lk, TransactionState::kCommittingWithPrepare);
- opCtx->recoveryUnit()->setCommitTimestamp(commitTimestamp);
-
- // We need to unlock the session to run the opObserver onTransactionCommit, which calls back
- // into the session.
- lk.unlock();
- auto opObserver = opCtx->getServiceContext()->getOpObserver();
- invariant(opObserver);
- opObserver->onTransactionCommit(opCtx, true /* wasPrepared */);
- lk.lock();
-
- _checkIsActiveTransaction(lk, *opCtx->getTxnNumber(), true);
- _commitTransaction(std::move(lk), opCtx);
-}
-
-void Session::_commitTransaction(stdx::unique_lock<stdx::mutex> lk, OperationContext* opCtx) {
- auto abortGuard = MakeGuard([this, opCtx]() {
- _abortActiveTransaction(opCtx,
- TransactionState::kCommittingWithoutPrepare |
- TransactionState::kCommittingWithPrepare);
- });
- lk.unlock();
- opCtx->getWriteUnitOfWork()->commit();
- opCtx->setWriteUnitOfWork(nullptr);
- abortGuard.Dismiss();
- lk.lock();
- auto& clientInfo = repl::ReplClientInfo::forClient(opCtx->getClient());
- // If no writes have been done, set the client optime forward to the read timestamp so waiting
- // for write concern will ensure all read data was committed.
- //
- // TODO(SERVER-34881): Once the default read concern is speculative majority, only set the
- // client optime forward if the original read concern level is "majority" or "snapshot".
- if (_speculativeTransactionReadOpTime > clientInfo.getLastOp()) {
- clientInfo.setLastOp(_speculativeTransactionReadOpTime);
- }
- _txnState.transitionTo(lk, TransactionState::kCommitted);
-
- ServerTransactionsMetrics::get(opCtx)->incrementTotalCommitted();
- // After the transaction has been committed, we must update the end time and mark it as
- // inactive.
- _singleTransactionStats->setEndTime(curTimeMicros64());
- if (_singleTransactionStats->isActive()) {
- _singleTransactionStats->setInactive(curTimeMicros64());
- }
- ServerTransactionsMetrics::get(opCtx)->decrementCurrentOpen();
- ServerTransactionsMetrics::get(getGlobalServiceContext())->decrementCurrentActive();
- // Add the latest operation stats to the aggregate OpDebug object stored in the
- // SingleTransactionStats instance on the Session.
- _singleTransactionStats->getOpDebug()->additiveMetrics.add(
- CurOp::get(opCtx)->debug().additiveMetrics);
- // Update the LastClientInfo object stored in the SingleTransactionStats instance on the Session
- // with this Client's information.
- _singleTransactionStats->updateLastClientInfo(opCtx->getClient());
-
- // Log the transaction if its duration is longer than the slowMS command threshold.
- _logSlowTransaction(lk,
- &(opCtx->lockState()->getLockerInfo())->stats,
- TransactionState::kCommitted,
- repl::ReadConcernArgs::get(opCtx));
-
- // We must clear the recovery unit and locker so any post-transaction writes can run without
- // transactional settings such as a read timestamp.
- _cleanUpTxnResourceOnOpCtx(opCtx);
-}
-
-BSONObj Session::reportStashedState() const {
- BSONObjBuilder builder;
- reportStashedState(&builder);
- return builder.obj();
-}
-
-void Session::reportStashedState(BSONObjBuilder* builder) const {
- stdx::lock_guard<stdx::mutex> ls(_mutex);
-
- if (_txnResourceStash && _txnResourceStash->locker()) {
- if (auto lockerInfo = _txnResourceStash->locker()->getLockerInfo()) {
- invariant(_activeTxnNumber != kUninitializedTxnNumber);
- builder->append("host", getHostNameCachedAndPort());
- builder->append("desc", "inactive transaction");
- auto lastClientInfo = _singleTransactionStats->getLastClientInfo();
- builder->append("client", lastClientInfo.clientHostAndPort);
- builder->append("connectionId", lastClientInfo.connectionId);
- builder->append("appName", lastClientInfo.appName);
- builder->append("clientMetadata", lastClientInfo.clientMetadata);
- {
- BSONObjBuilder lsid(builder->subobjStart("lsid"));
- getSessionId().serialize(&lsid);
- }
- BSONObjBuilder transactionBuilder;
- _reportTransactionStats(
- ls, &transactionBuilder, _txnResourceStash->getReadConcernArgs());
- builder->append("transaction", transactionBuilder.obj());
- builder->append("waitingForLock", false);
- builder->append("active", false);
- fillLockerInfo(*lockerInfo, *builder);
- }
- }
-}
-
-void Session::reportUnstashedState(repl::ReadConcernArgs readConcernArgs,
- BSONObjBuilder* builder) const {
- stdx::lock_guard<stdx::mutex> ls(_mutex);
-
- if (!_txnResourceStash) {
- BSONObjBuilder transactionBuilder;
- _reportTransactionStats(ls, &transactionBuilder, readConcernArgs);
- builder->append("transaction", transactionBuilder.obj());
- }
-}
-
-void Session::_reportTransactionStats(WithLock wl,
- BSONObjBuilder* builder,
- repl::ReadConcernArgs readConcernArgs) const {
- BSONObjBuilder parametersBuilder(builder->subobjStart("parameters"));
- parametersBuilder.append("txnNumber", _activeTxnNumber);
-
- if (!_txnState.inMultiDocumentTransaction(wl)) {
- // For retryable writes, we only include the txnNumber.
- parametersBuilder.done();
- return;
- }
- parametersBuilder.append("autocommit", _autocommit);
- readConcernArgs.appendInfo(&parametersBuilder);
- parametersBuilder.done();
-
- builder->append("readTimestamp", _speculativeTransactionReadOpTime.getTimestamp());
- builder->append("startWallClockTime",
- dateToISOStringLocal(Date_t::fromMillisSinceEpoch(
- _singleTransactionStats->getStartTime() / 1000)));
- // We use the same "now" time so that the following time metrics are consistent with each other.
- auto curTime = curTimeMicros64();
- builder->append("timeOpenMicros",
- static_cast<long long>(_singleTransactionStats->getDuration(curTime)));
- auto timeActive =
- durationCount<Microseconds>(_singleTransactionStats->getTimeActiveMicros(curTime));
- auto timeInactive =
- durationCount<Microseconds>(_singleTransactionStats->getTimeInactiveMicros(curTime));
- builder->append("timeActiveMicros", timeActive);
- builder->append("timeInactiveMicros", timeInactive);
-
- if (_transactionExpireDate) {
- builder->append("expiryTime", dateToISOStringLocal(*_transactionExpireDate));
- }
-}
-
-std::string Session::_transactionInfoForLog(const SingleThreadedLockStats* lockStats,
- TransactionState::StateFlag terminationCause,
- repl::ReadConcernArgs readConcernArgs) {
- invariant(lockStats);
- invariant(terminationCause == TransactionState::kCommitted ||
- terminationCause == TransactionState::kAborted);
-
- StringBuilder s;
-
- // User specified transaction parameters.
- BSONObjBuilder parametersBuilder;
- BSONObjBuilder lsidBuilder(parametersBuilder.subobjStart("lsid"));
- _sessionId.serialize(&lsidBuilder);
- lsidBuilder.doneFast();
- parametersBuilder.append("txnNumber", _activeTxnNumber);
- parametersBuilder.append("autocommit", _autocommit);
- readConcernArgs.appendInfo(&parametersBuilder);
- s << "parameters:" << parametersBuilder.obj().toString() << ",";
-
- s << " readTimestamp:" << _speculativeTransactionReadOpTime.getTimestamp().toString() << ",";
-
- s << _singleTransactionStats->getOpDebug()->additiveMetrics.report();
-
- std::string terminationCauseString =
- terminationCause == TransactionState::kCommitted ? "committed" : "aborted";
- s << " terminationCause:" << terminationCauseString;
-
- auto curTime = curTimeMicros64();
- s << " timeActiveMicros:"
- << durationCount<Microseconds>(_singleTransactionStats->getTimeActiveMicros(curTime));
- s << " timeInactiveMicros:"
- << durationCount<Microseconds>(_singleTransactionStats->getTimeInactiveMicros(curTime));
-
- // Number of yields is always 0 in multi-document transactions, but it is included mainly to
- // match the format with other slow operation logging messages.
- s << " numYields:" << 0;
-
- // Aggregate lock statistics.
- BSONObjBuilder locks;
- lockStats->report(&locks);
- s << " locks:" << locks.obj().toString();
-
- // Total duration of the transaction.
- s << " "
- << Milliseconds{static_cast<long long>(_singleTransactionStats->getDuration(curTime)) / 1000};
-
- return s.str();
-}
-
-void Session::_logSlowTransaction(WithLock wl,
- const SingleThreadedLockStats* lockStats,
- TransactionState::StateFlag terminationCause,
- repl::ReadConcernArgs readConcernArgs) {
- // Only log multi-document transactions.
- if (!_txnState.isNone(wl)) {
- // Log the transaction if its duration is longer than the slowMS command threshold.
- if (_singleTransactionStats->getDuration(curTimeMicros64()) >
- serverGlobalParams.slowMS * 1000ULL) {
- log(logger::LogComponent::kTransaction)
- << "transaction "
- << _transactionInfoForLog(lockStats, terminationCause, readConcernArgs);
- }
- }
}
void Session::_checkValid(WithLock) const {
@@ -1391,7 +472,7 @@ void Session::_checkValid(WithLock) const {
_isValid);
}
-void Session::_checkIsActiveTransaction(WithLock wl, TxnNumber txnNumber, bool checkAbort) const {
+void Session::_checkIsActiveTransaction(WithLock, TxnNumber txnNumber) const {
uassert(ErrorCodes::ConflictingOperationInProgress,
str::stream() << "Cannot perform operations on transaction " << txnNumber
<< " on session "
@@ -1400,20 +481,13 @@ void Session::_checkIsActiveTransaction(WithLock wl, TxnNumber txnNumber, bool c
<< _activeTxnNumber
<< " is now active.",
txnNumber == _activeTxnNumber);
-
- uassert(ErrorCodes::NoSuchTransaction,
- str::stream() << "Transaction " << txnNumber << " has been aborted.",
- !checkAbort || !_txnState.isAborted(wl));
}
boost::optional<repl::OpTime> Session::_checkStatementExecuted(WithLock wl,
TxnNumber txnNumber,
StmtId stmtId) const {
_checkValid(wl);
- _checkIsActiveTransaction(wl, txnNumber, false);
- // Retries are not detected for multi-document transactions.
- if (!_txnState.isNone(wl))
- return boost::none;
+ _checkIsActiveTransaction(wl, txnNumber);
const auto it = _activeTxnCommittedStatements.find(stmtId);
if (it == _activeTxnCommittedStatements.end()) {
@@ -1434,7 +508,7 @@ boost::optional<repl::OpTime> Session::_checkStatementExecuted(WithLock wl,
Date_t Session::_getLastWriteDate(WithLock wl, TxnNumber txnNumber) const {
_checkValid(wl);
- _checkIsActiveTransaction(wl, txnNumber, false);
+ _checkIsActiveTransaction(wl, txnNumber);
if (!_lastWrittenSessionRecord || _lastWrittenSessionRecord->getTxnNum() != txnNumber)
return {};
@@ -1500,7 +574,7 @@ void Session::_registerUpdateCacheOnCommit(OperationContext* opCtx,
// entry gets invalidated and immediately refreshed while there were no writes for
// newTxnNumber yet. In this case _activeTxnNumber will be less than newTxnNumber
// and we will fail to update the cache even though the write was successful.
- _beginOrContinueTxn(lg, newTxnNumber, boost::none, boost::none);
+ _beginOrContinueTxn(lg, newTxnNumber);
}
if (newTxnNumber == _activeTxnNumber) {
@@ -1581,102 +655,30 @@ boost::optional<repl::OplogEntry> Session::createMatchingTransactionTableUpdate(
);
}
-std::string Session::TransactionState::toString(StateFlag state) {
- switch (state) {
- case Session::TransactionState::kNone:
- return "TxnState::None";
- case Session::TransactionState::kInProgress:
- return "TxnState::InProgress";
- case Session::TransactionState::kPrepared:
- return "TxnState::Prepared";
- case Session::TransactionState::kCommittingWithoutPrepare:
- return "TxnState::CommittingWithoutPrepare";
- case Session::TransactionState::kCommittingWithPrepare:
- return "TxnState::CommittingWithPrepare";
- case Session::TransactionState::kCommitted:
- return "TxnState::Committed";
- case Session::TransactionState::kAborted:
- return "TxnState::Aborted";
- }
- MONGO_UNREACHABLE;
+boost::optional<Session::RefreshState> Session::getLastRefreshState() const {
+ stdx::lock_guard<stdx::mutex> lg(_mutex);
+ return _lastRefreshState;
}
-bool Session::TransactionState::_isLegalTransition(StateFlag oldState, StateFlag newState) {
- switch (oldState) {
- case kNone:
- switch (newState) {
- case kNone:
- case kInProgress:
- return true;
- default:
- return false;
- }
- MONGO_UNREACHABLE;
- case kInProgress:
- switch (newState) {
- case kNone:
- case kPrepared:
- case kCommittingWithoutPrepare:
- case kAborted:
- return true;
- default:
- return false;
- }
- MONGO_UNREACHABLE;
- case kPrepared:
- switch (newState) {
- case kCommittingWithPrepare:
- case kAborted:
- return true;
- default:
- return false;
- }
- MONGO_UNREACHABLE;
- case kCommittingWithPrepare:
- case kCommittingWithoutPrepare:
- switch (newState) {
- case kNone:
- case kCommitted:
- case kAborted:
- return true;
- default:
- return false;
- }
- MONGO_UNREACHABLE;
- case kCommitted:
- switch (newState) {
- case kNone:
- case kInProgress:
- return true;
- default:
- return false;
- }
- MONGO_UNREACHABLE;
- case kAborted:
- switch (newState) {
- case kNone:
- case kInProgress:
- return true;
- default:
- return false;
- }
- MONGO_UNREACHABLE;
- }
- MONGO_UNREACHABLE;
-}
+void Session::lockTxnNumber(const TxnNumber lockThisNumber, Status conflictError) {
+ stdx::lock_guard<stdx::mutex> lg(_mutex);
+ uassert(50907,
+ str::stream() << "cannot lock txnNumber to " << lockThisNumber
+ << " because current txnNumber is "
+ << _activeTxnNumber,
+ _activeTxnNumber == lockThisNumber);
+ // TODO: remove this if we need to support recursive locking.
+ invariant(!_isTxnNumberLocked);
-void Session::TransactionState::transitionTo(WithLock,
- StateFlag newState,
- TransitionValidation shouldValidate) {
+ _isTxnNumberLocked = true;
+ _txnNumberLockConflictStatus = conflictError;
+}
- if (shouldValidate == TransitionValidation::kValidateTransition) {
- invariant(TransactionState::_isLegalTransition(_state, newState),
- str::stream() << "Current state: " << toString(_state)
- << ", Illegal attempted next state: "
- << toString(newState));
- }
+void Session::unlockTxnNumber() {
+ stdx::lock_guard<stdx::mutex> lg(_mutex);
- _state = newState;
+ _isTxnNumberLocked = false;
+ _txnNumberLockConflictStatus = boost::none;
}
} // namespace mongo
diff --git a/src/mongo/db/session.h b/src/mongo/db/session.h
index 0e9c3444de3..835d264f5a8 100644
--- a/src/mongo/db/session.h
+++ b/src/mongo/db/session.h
@@ -32,24 +32,17 @@
#include "mongo/base/disallow_copying.h"
#include "mongo/bson/timestamp.h"
-#include "mongo/db/concurrency/locker.h"
#include "mongo/db/logical_session_id.h"
-#include "mongo/db/multi_key_path_tracker.h"
#include "mongo/db/operation_context.h"
#include "mongo/db/repl/oplog.h"
#include "mongo/db/repl/oplog_entry.h"
-#include "mongo/db/repl/read_concern_args.h"
#include "mongo/db/session_txn_record_gen.h"
-#include "mongo/db/single_transaction_stats.h"
-#include "mongo/db/storage/recovery_unit.h"
#include "mongo/platform/atomic_word.h"
#include "mongo/stdx/unordered_map.h"
#include "mongo/util/concurrency/with_lock.h"
namespace mongo {
-extern AtomicInt32 transactionLifetimeLimitSeconds;
-
class OperationContext;
class UpdateRequest;
@@ -65,73 +58,7 @@ class Session : public Decorable<Session> {
MONGO_DISALLOW_COPYING(Session);
public:
- /**
- * Holds state for a snapshot read or multi-statement transaction in between network operations.
- */
- class TxnResources {
- public:
- /**
- * Stashes transaction state from 'opCtx' in the newly constructed TxnResources.
- * keepTicket: If true, do not release locker's throttling ticket.
- * Use only for short-term stashing.
- */
- TxnResources(OperationContext* opCtx, bool keepTicket = false);
-
- ~TxnResources();
-
- // Rule of 5: because we have a class-defined destructor, we need to explictly specify
- // the move operator and move assignment operator.
- TxnResources(TxnResources&&) = default;
- TxnResources& operator=(TxnResources&&) = default;
-
- /**
- * Returns a const pointer to the stashed lock state, or nullptr if no stashed locks exist.
- */
- const Locker* locker() const {
- return _locker.get();
- }
-
- /**
- * Releases stashed transaction state onto 'opCtx'. Must only be called once.
- */
- void release(OperationContext* opCtx);
-
- /**
- * Returns the read concern arguments.
- */
- repl::ReadConcernArgs getReadConcernArgs() const {
- return _readConcernArgs;
- }
-
- private:
- bool _released = false;
- std::unique_ptr<Locker> _locker;
- std::unique_ptr<RecoveryUnit> _recoveryUnit;
- repl::ReadConcernArgs _readConcernArgs;
- WriteUnitOfWork::RecoveryUnitState _ruState;
- };
-
- /**
- * An RAII object that stashes `TxnResouces` from the `opCtx` onto the stack. At destruction
- * it unstashes the `TxnResources` back onto the `opCtx`.
- */
- class SideTransactionBlock {
- public:
- SideTransactionBlock(OperationContext* opCtx);
- ~SideTransactionBlock();
-
- // Rule of 5: because we have a class-defined destructor, we need to explictly specify
- // the move operator and move assignment operator.
- SideTransactionBlock(SideTransactionBlock&&) = default;
- SideTransactionBlock& operator=(SideTransactionBlock&&) = default;
-
- private:
- boost::optional<Session::TxnResources> _txnResources;
- OperationContext* _opCtx;
- };
-
using CommittedStatementTimestampMap = stdx::unordered_map<StmtId, repl::OpTime>;
- using CursorExistsFunction = std::function<bool(LogicalSessionId, TxnNumber)>;
static const BSONObj kDeadEndSentinel;
@@ -141,6 +68,12 @@ public:
return _sessionId;
}
+ struct RefreshState {
+ long long refreshCount{0};
+ TxnNumber txnNumber{kUninitializedTxnNumber};
+ bool isCommitted{false};
+ };
+
/**
* Blocking method, which loads the transaction state from storage if it has been marked as
* needing refresh.
@@ -153,43 +86,16 @@ public:
/**
* Starts a new transaction on the session, or continues an already active transaction. In this
* context, a "transaction" is a sequence of operations associated with a transaction number.
- * This sequence of operations could be a retryable write or multi-statement transaction. Both
- * utilize this method.
- *
- * The 'autocommit' argument represents the value of the field given in the original client
- * request. If it is boost::none, no autocommit parameter was passed into the request. Every
- * operation that is part of a multi statement transaction must specify 'autocommit=false'.
- * 'startTransaction' represents the value of the field given in the original client request,
- * and indicates whether this operation is the beginning of a multi-statement transaction.
*
* Throws an exception if:
* - An attempt is made to start a transaction with number less than the latest
* transaction this session has seen.
* - The session has been invalidated.
- * - The values of 'autocommit' and/or 'startTransaction' are inconsistent with the current
- * state of the transaction.
*
* In order to avoid the possibility of deadlock, this method must not be called while holding a
* lock. This method must also be called after refreshFromStorageIfNeeded has been called.
*/
- void beginOrContinueTxn(OperationContext* opCtx,
- TxnNumber txnNumber,
- boost::optional<bool> autocommit,
- boost::optional<bool> startTransaction,
- StringData dbName,
- StringData cmdName);
- /**
- * Similar to beginOrContinueTxn except it is used specifically for shard migrations and does
- * not check or modify the autocommit parameter.
- *
- * Not called with session checked out.
- */
- void beginOrContinueTxnOnMigration(OperationContext* opCtx, TxnNumber txnNumber);
-
- /**
- * Called for speculative transactions to fix the optime of the snapshot to read from.
- */
- void setSpeculativeTransactionOpTimeToLastApplied(OperationContext* opCtx);
+ void beginOrContinueTxn(OperationContext* opCtx, TxnNumber txnNumber);
/**
* Called after a write under the specified transaction completes while the node is a primary
@@ -271,157 +177,11 @@ public:
*/
bool checkStatementExecutedNoOplogEntryFetch(TxnNumber txnNumber, StmtId stmtId) const;
- /**
- * Transfers management of transaction resources from the OperationContext to the Session.
- */
- void stashTransactionResources(OperationContext* opCtx);
-
- /**
- * Transfers management of transaction resources from the Session to the OperationContext.
- */
- void unstashTransactionResources(OperationContext* opCtx, const std::string& cmdName);
-
- /**
- * Commits the transaction, including committing the write unit of work and updating
- * transaction state.
- *
- * Throws an exception if the transaction is prepared.
- */
- void commitUnpreparedTransaction(OperationContext* opCtx);
-
- /**
- * Commits the transaction, including committing the write unit of work and updating
- * transaction state.
- *
- * Throws an exception if the transaction is not prepared or if the 'commitTimestamp' is null.
- */
- void commitPreparedTransaction(OperationContext* opCtx, Timestamp commitTimestamp);
-
- /**
- * Puts a transaction into a prepared state and returns the prepareTimestamp.
- */
- Timestamp prepareTransaction(OperationContext* opCtx);
-
- /**
- * Aborts the transaction outside the transaction, releasing transaction resources.
- *
- * Not called with session checked out.
- */
- void abortArbitraryTransaction();
-
- /**
- * Same as abortArbitraryTransaction, except only executes if _transactionExpireDate indicates
- * that the transaction has expired.
- *
- * Not called with session checked out.
- */
- void abortArbitraryTransactionIfExpired();
-
- /*
- * Aborts the transaction inside the transaction, releasing transaction resources.
- * We're inside the transaction when we have the Session checked out and 'opCtx' owns the
- * transaction resources.
- */
- void abortActiveTransaction(OperationContext* opCtx);
-
- bool getAutocommit() const {
- return _autocommit;
- }
-
- /**
- * Returns whether we are in a multi-document transaction, which means we have an active
- * transaction which has autoCommit:false and has not been committed or aborted. It is possible
- * that the current transaction is stashed onto the stack via a `SideTransactionBlock`.
- */
- bool inMultiDocumentTransaction() const {
- stdx::lock_guard<stdx::mutex> lk(_mutex);
- return _txnState.inMultiDocumentTransaction(lk);
- };
-
- bool transactionIsCommitted() const {
- stdx::lock_guard<stdx::mutex> lk(_mutex);
- return _txnState.isCommitted(lk);
- }
-
- bool transactionIsAborted() const {
- stdx::lock_guard<stdx::mutex> lk(_mutex);
- return _txnState.isAborted(lk);
- }
-
- /**
- * Adds a stored operation to the list of stored operations for the current multi-document
- * (non-autocommit) transaction. It is illegal to add operations when no multi-document
- * transaction is in progress.
- */
- void addTransactionOperation(OperationContext* opCtx, const repl::ReplOperation& operation);
-
- /**
- * Returns and clears the stored operations for an multi-document (non-autocommit) transaction,
- * and marks the transaction as closed. It is illegal to attempt to add operations to the
- * transaction after this is called.
- */
- std::vector<repl::ReplOperation> endTransactionAndRetrieveOperations(OperationContext* opCtx);
-
- const std::vector<repl::ReplOperation>& transactionOperationsForTest() {
- return _transactionOperations;
- }
-
- TxnNumber getActiveTxnNumberForTest() const {
+ TxnNumber getActiveTxnNumber() const {
stdx::lock_guard<stdx::mutex> lk(_mutex);
return _activeTxnNumber;
}
- boost::optional<SingleTransactionStats> getSingleTransactionStats() const {
- return _singleTransactionStats;
- }
-
- repl::OpTime getSpeculativeTransactionReadOpTimeForTest() const {
- stdx::lock_guard<stdx::mutex> lk(_mutex);
- return _speculativeTransactionReadOpTime;
- }
-
- const Locker* getTxnResourceStashLockerForTest() const {
- stdx::lock_guard<stdx::mutex> lk(_mutex);
- invariant(_txnResourceStash);
- return _txnResourceStash->locker();
- }
-
- /**
- * If this session is holding stashed locks in _txnResourceStash, reports the current state of
- * the session using the provided builder. Locks the session object's mutex while running.
- */
- void reportStashedState(BSONObjBuilder* builder) const;
-
- /**
- * If this session is not holding stashed locks in _txnResourceStash (transaction is active),
- * reports the current state of the session using the provided builder. Locks the session
- * object's mutex while running.
- */
- void reportUnstashedState(repl::ReadConcernArgs readConcernArgs, BSONObjBuilder* builder) const;
-
- /**
- * Convenience method which creates and populates a BSONObj containing the stashed state.
- * Returns an empty BSONObj if this session has no stashed resources.
- */
- BSONObj reportStashedState() const;
-
- std::string transactionInfoForLogForTest(const SingleThreadedLockStats* lockStats,
- bool committed,
- repl::ReadConcernArgs readConcernArgs) {
- stdx::lock_guard<stdx::mutex> lk(_mutex);
- TransactionState::StateFlag terminationCause =
- committed ? TransactionState::kCommitted : TransactionState::kAborted;
- return _transactionInfoForLog(lockStats, terminationCause, readConcernArgs);
- }
-
- void addMultikeyPathInfo(MultikeyPathInfo info) {
- _multikeyPathInfo.push_back(std::move(info));
- }
-
- const std::vector<MultikeyPathInfo>& getMultikeyPathInfo() const {
- return _multikeyPathInfo;
- }
-
/**
* Returns a new oplog entry if the given entry has transaction state embedded within in.
* The new oplog entry will contain the operation needed to replicate the transaction
@@ -432,27 +192,25 @@ public:
static boost::optional<repl::OplogEntry> createMatchingTransactionTableUpdate(
const repl::OplogEntry& entry);
- void transitionToPreparedforTest() {
- stdx::lock_guard<stdx::mutex> lk(_mutex);
- _txnState.transitionTo(lk, TransactionState::kPrepared);
- }
-
- void transitionToCommittingforTest() {
- stdx::lock_guard<stdx::mutex> lk(_mutex);
- _txnState.transitionTo(lk, TransactionState::kCommittingWithoutPrepare);
- }
+ /**
+ * Returns the state of the session from storage the last time a refresh occurred.
+ */
+ boost::optional<RefreshState> getLastRefreshState() const;
-private:
- // Holds function which determines whether the CursorManager has client cursor references for a
- // given transaction.
- static CursorExistsFunction _cursorExistsFunction;
+ /**
+ * Attempt to lock the active TxnNumber of this session to the given number. This operation
+ * can only succeed if it is equal to the current active TxnNumber. Also sets the error status
+ * for any callers trying to modify the TxnNumber.
+ */
+ void lockTxnNumber(const TxnNumber lockThisNumber, Status conflictError);
- void _beginOrContinueTxn(WithLock,
- TxnNumber txnNumber,
- boost::optional<bool> autocommit,
- boost::optional<bool> startTransaction);
+ /**
+ * Release the lock on the active TxnNumber and allow it to be modified.
+ */
+ void unlockTxnNumber();
- void _beginOrContinueTxnOnMigration(WithLock, TxnNumber txnNumber);
+private:
+ void _beginOrContinueTxn(WithLock, TxnNumber txnNumber);
// Checks if there is a conflicting operation on the current Session
void _checkValid(WithLock) const;
@@ -463,7 +221,7 @@ private:
void _setActiveTxn(WithLock, TxnNumber txnNumber);
- void _checkIsActiveTransaction(WithLock, TxnNumber txnNumber, bool checkAbort) const;
+ void _checkIsActiveTransaction(WithLock, TxnNumber txnNumber) const;
boost::optional<repl::OpTime> _checkStatementExecuted(WithLock,
TxnNumber txnNumber,
@@ -485,149 +243,6 @@ private:
std::vector<StmtId> stmtIdsWritten,
const repl::OpTime& lastStmtIdWriteTs);
- /**
- * Reserves a slot in the oplog with an open storage-transaction while it is alive. Reserves the
- * slot at construction. Aborts the storage-transaction and releases the oplog slot at
- * destruction.
- */
- class OplogSlotReserver {
- public:
- OplogSlotReserver(OperationContext* opCtx);
-
- ~OplogSlotReserver();
-
- // Rule of 5: because we have a class-defined destructor, we need to explictly specify
- // the move operator and move assignment operator.
- OplogSlotReserver(OplogSlotReserver&&) = default;
- OplogSlotReserver& operator=(OplogSlotReserver&&) = default;
-
- /**
- * Returns the oplog slot reserved at construction.
- */
- OplogSlot getReservedOplogSlot() const {
- invariant(!_oplogSlot.opTime.isNull());
- return _oplogSlot;
- }
-
- private:
- std::unique_ptr<Locker> _locker;
- std::unique_ptr<RecoveryUnit> _recoveryUnit;
- OplogSlot _oplogSlot;
- };
-
- /**
- * Indicates the state of the current multi-document transaction, if any. If the transaction is
- * in any state but kInProgress, no more operations can be collected. Once the transaction is in
- * kPrepared, the transaction is not allowed to abort outside of an 'abortTransaction' command.
- * At this point, aborting the transaction must log an 'abortTransaction' oplog entry.
- */
- class TransactionState {
- public:
- enum StateFlag {
- kNone = 1 << 0,
- kInProgress = 1 << 1,
- kPrepared = 1 << 2,
- kCommittingWithoutPrepare = 1 << 3,
- kCommittingWithPrepare = 1 << 4,
- kCommitted = 1 << 5,
- kAborted = 1 << 6
- };
-
- using StateSet = int;
-
- bool isInSet(WithLock, StateSet stateSet) const {
- return _state & stateSet;
- }
-
- /**
- * Transitions the session from the current state to the new state. If transition validation
- * is not relaxed, invariants if the transition is illegal.
- */
- enum class TransitionValidation { kValidateTransition, kRelaxTransitionValidation };
- void transitionTo(
- WithLock,
- StateFlag newState,
- TransitionValidation shouldValidate = TransitionValidation::kValidateTransition);
-
- bool inMultiDocumentTransaction(WithLock) const {
- return _state == kInProgress || _state == kPrepared;
- }
-
- bool isNone(WithLock) const {
- return _state == kNone;
- }
-
- bool isInProgress(WithLock) const {
- return _state == kInProgress;
- }
-
- bool isPrepared(WithLock) const {
- return _state == kPrepared;
- }
-
- bool isCommittingWithoutPrepare(WithLock) const {
- return _state == kCommittingWithoutPrepare;
- }
-
- bool isCommittingWithPrepare(WithLock) const {
- return _state == kCommittingWithPrepare;
- }
-
- bool isCommitted(WithLock) const {
- return _state == kCommitted;
- }
-
- bool isAborted(WithLock) const {
- return _state == kAborted;
- }
-
- std::string toString() const {
- return toString(_state);
- }
-
- static std::string toString(StateFlag state);
-
- private:
- static bool _isLegalTransition(StateFlag oldState, StateFlag newState);
-
- StateFlag _state = kNone;
- };
-
- friend std::ostream& operator<<(std::ostream& s, TransactionState txnState) {
- return (s << txnState.toString());
- }
-
- friend StringBuilder& operator<<(StringBuilder& s, TransactionState txnState) {
- return (s << txnState.toString());
- }
-
- // Abort the transaction if it's in one of the expected states and clean up the transaction
- // states associated with the opCtx.
- void _abortActiveTransaction(OperationContext* opCtx,
- TransactionState::StateSet expectedStates);
-
- void _abortArbitraryTransaction(WithLock);
-
- // Releases stashed transaction resources to abort the transaction on the session.
- void _abortTransactionOnSession(WithLock);
-
- // Clean up the transaction resources unstashed on operation context.
- void _cleanUpTxnResourceOnOpCtx(OperationContext* opCtx);
-
- // Committing a transaction first changes its state to "Committing*" and writes to the oplog,
- // then it changes the state to "Committed".
- //
- // When a transaction is in "Committing*" state, it's not allowed for other threads to change
- // its state (i.e. abort the transaction), otherwise the on-disk state will diverge from the
- // in-memory state.
- // There are 3 cases where the transaction will be aborted.
- // 1) abortTransaction command. Session check-out mechanism only allows one client to access a
- // transaction.
- // 2) killSession, stepdown, transaction timeout and any thread that aborts the transaction
- // outside of session checkout. They can safely skip the committing transactions.
- // 3) Migration. Should be able to skip committing transactions.
- void _commitTransaction(stdx::unique_lock<stdx::mutex> lk, OperationContext* opCtx);
-
const LogicalSessionId _sessionId;
// Protects the member variables below.
@@ -644,27 +259,6 @@ private:
// truncated because it was too old.
bool _hasIncompleteHistory{false};
- // Logs the transaction information if it has run slower than the global parameter slowMS. The
- // transaction must be committed or aborted when this function is called.
- void _logSlowTransaction(WithLock wl,
- const SingleThreadedLockStats* lockStats,
- TransactionState::StateFlag terminationCause,
- repl::ReadConcernArgs readConcernArgs);
-
- // This method returns a string with information about a slow transaction. The format of the
- // logging string produced should match the format used for slow operation logging. A
- // transaction must be completed (committed or aborted) and a valid LockStats reference must be
- // passed in order for this method to be called.
- std::string _transactionInfoForLog(const SingleThreadedLockStats* lockStats,
- TransactionState::StateFlag terminationCause,
- repl::ReadConcernArgs readConcernArgs);
-
- // Reports transaction stats for both active and inactive transactions using the provided
- // builder.
- void _reportTransactionStats(WithLock wl,
- BSONObjBuilder* builder,
- repl::ReadConcernArgs readConcernArgs) const;
-
// Caches what is known to be the last written transaction record for the session
boost::optional<SessionTxnRecord> _lastWrittenSessionRecord;
@@ -673,44 +267,20 @@ private:
// means a new transaction has begun on the session, but it hasn't yet performed any writes.
TxnNumber _activeTxnNumber{kUninitializedTxnNumber};
- // Holds transaction resources between network operations.
- boost::optional<TxnResources> _txnResourceStash;
-
- // Maintains the transaction state and the transition table for legal state transitions.
- TransactionState _txnState;
-
- // Holds oplog data for operations which have been applied in the current multi-document
- // transaction. Not used for retryable writes.
- std::vector<repl::ReplOperation> _transactionOperations;
-
- // Total size in bytes of all operations within the _transactionOperations vector.
- size_t _transactionOperationBytes = 0;
-
// For the active txn, tracks which statement ids have been committed and at which oplog
// opTime. Used for fast retryability check and retrieving the previous write's data without
// having to scan through the oplog.
CommittedStatementTimestampMap _activeTxnCommittedStatements;
- // Set in _beginOrContinueTxn and applies to the activeTxn on the session.
- bool _autocommit{true};
-
- // Set when a snapshot read / transaction begins. Alleviates cache pressure by limiting how long
- // a snapshot will remain open and available. Checked in combination with _txnState to determine
- // whether the transaction should be aborted.
- // This is unset until a transaction begins on the session, and then reset only when new
- // transactions begin.
- boost::optional<Date_t> _transactionExpireDate;
-
- // The OpTime a speculative transaction is reading from and also the earliest opTime it
- // should wait for write concern for on commit.
- repl::OpTime _speculativeTransactionReadOpTime;
+ // Stores the state from last refresh.
+ boost::optional<RefreshState> _lastRefreshState;
- // This member is only applicable to operations running in a transaction. It is reset when a
- // transaction state resets.
- std::vector<MultikeyPathInfo> _multikeyPathInfo;
+ // True if txnNumber cannot be modified.
+ bool _isTxnNumberLocked{false};
- // Tracks metrics for a single multi-document transaction. Not used for retryable writes.
- boost::optional<SingleTransactionStats> _singleTransactionStats;
+ // The status to return when an operation tries to modify the active TxnNumber while it is
+ // locked.
+ boost::optional<Status> _txnNumberLockConflictStatus;
};
} // namespace mongo
diff --git a/src/mongo/db/session_catalog.cpp b/src/mongo/db/session_catalog.cpp
index 24fcea6b37e..ec33b9abfb1 100644
--- a/src/mongo/db/session_catalog.cpp
+++ b/src/mongo/db/session_catalog.cpp
@@ -40,6 +40,7 @@
#include "mongo/db/namespace_string.h"
#include "mongo/db/operation_context.h"
#include "mongo/db/service_context.h"
+#include "mongo/db/transaction_participant.h"
#include "mongo/rpc/get_status_from_command_result.h"
#include "mongo/stdx/memory.h"
#include "mongo/util/log.h"
@@ -150,9 +151,6 @@ ScopedSession SessionCatalog::getOrCreateSession(OperationContext* opCtx,
return ScopedSession(_getOrCreateSessionRuntimeInfo(ul, opCtx, lsid));
}();
- // Perform the refresh outside of the mutex
- ss->refreshFromStorageIfNeeded(opCtx);
-
return ss;
}
@@ -239,14 +237,8 @@ void SessionCatalog::_releaseSession(const LogicalSessionId& lsid) {
sri->availableCondVar.notify_one();
}
-OperationContextSession::OperationContextSession(OperationContext* opCtx,
- bool checkOutSession,
- boost::optional<bool> autocommit,
- boost::optional<bool> startTransaction,
- StringData dbName,
- StringData cmdName)
+OperationContextSession::OperationContextSession(OperationContext* opCtx, bool checkOutSession)
: _opCtx(opCtx) {
-
if (!opCtx->getLogicalSessionId()) {
return;
}
@@ -272,13 +264,6 @@ OperationContextSession::OperationContextSession(OperationContext* opCtx,
const auto session = checkedOutSession->get();
invariant(opCtx->getLogicalSessionId() == session->getSessionId());
-
- checkedOutSession->get()->refreshFromStorageIfNeeded(opCtx);
-
- if (opCtx->getTxnNumber()) {
- checkedOutSession->get()->beginOrContinueTxn(
- opCtx, *opCtx->getTxnNumber(), autocommit, startTransaction, dbName, cmdName);
- }
}
OperationContextSession::~OperationContextSession() {
diff --git a/src/mongo/db/session_catalog.h b/src/mongo/db/session_catalog.h
index 18e33d8b385..963ad69a3ae 100644
--- a/src/mongo/db/session_catalog.h
+++ b/src/mongo/db/session_catalog.h
@@ -247,12 +247,7 @@ class OperationContextSession {
MONGO_DISALLOW_COPYING(OperationContextSession);
public:
- OperationContextSession(OperationContext* opCtx,
- bool checkOutSession,
- boost::optional<bool> autocommit,
- boost::optional<bool> startTransaction,
- StringData dbName,
- StringData cmdName);
+ OperationContextSession(OperationContext* opCtx, bool checkOutSession);
~OperationContextSession();
diff --git a/src/mongo/db/session_catalog_test.cpp b/src/mongo/db/session_catalog_test.cpp
index 1d4ba8ec87d..c55c2c780db 100644
--- a/src/mongo/db/session_catalog_test.cpp
+++ b/src/mongo/db/session_catalog_test.cpp
@@ -90,7 +90,7 @@ TEST_F(SessionCatalogTest, OperationContextCheckedOutSession) {
const TxnNumber txnNum = 20;
opCtx()->setTxnNumber(txnNum);
- OperationContextSession ocs(opCtx(), true, boost::none, boost::none, "testDB", "insert");
+ OperationContextSession ocs(opCtx(), true);
auto session = OperationContextSession::get(opCtx());
ASSERT(session);
ASSERT_EQ(*opCtx()->getLogicalSessionId(), session->getSessionId());
@@ -99,7 +99,7 @@ TEST_F(SessionCatalogTest, OperationContextCheckedOutSession) {
TEST_F(SessionCatalogTest, OperationContextNonCheckedOutSession) {
opCtx()->setLogicalSessionId(makeLogicalSessionIdForTest());
- OperationContextSession ocs(opCtx(), false, boost::none, boost::none, "testDB", "insert");
+ OperationContextSession ocs(opCtx(), false);
auto session = OperationContextSession::get(opCtx());
ASSERT(!session);
@@ -118,7 +118,7 @@ TEST_F(SessionCatalogTest, GetOrCreateSessionAfterCheckOutSession) {
opCtx()->setLogicalSessionId(lsid);
boost::optional<OperationContextSession> ocs;
- ocs.emplace(opCtx(), true, boost::none, false, "testDB", "insert");
+ ocs.emplace(opCtx(), true);
stdx::async(stdx::launch::async, [&] {
ON_BLOCK_EXIT([&] { Client::destroy(); });
@@ -149,13 +149,11 @@ TEST_F(SessionCatalogTest, NestedOperationContextSession) {
opCtx()->setLogicalSessionId(makeLogicalSessionIdForTest());
{
- OperationContextSession outerScopedSession(
- opCtx(), true, boost::none, boost::none, "testDB", "insert");
+ OperationContextSession outerScopedSession(opCtx(), true);
{
DirectClientSetter inDirectClient(opCtx());
- OperationContextSession innerScopedSession(
- opCtx(), true, boost::none, boost::none, "testDB", "insert");
+ OperationContextSession innerScopedSession(opCtx(), true);
auto session = OperationContextSession::get(opCtx());
ASSERT(session);
@@ -173,89 +171,6 @@ TEST_F(SessionCatalogTest, NestedOperationContextSession) {
ASSERT(!OperationContextSession::get(opCtx()));
}
-TEST_F(SessionCatalogTest, StashInNestedSessionIsANoop) {
- opCtx()->setLogicalSessionId(makeLogicalSessionIdForTest());
- opCtx()->setTxnNumber(1);
-
- {
- OperationContextSession outerScopedSession(
- opCtx(), true, /* autocommit */ false, /* startTransaction */ true, "testDB", "find");
-
- Locker* originalLocker = opCtx()->lockState();
- RecoveryUnit* originalRecoveryUnit = opCtx()->recoveryUnit();
- ASSERT(originalLocker);
- ASSERT(originalRecoveryUnit);
-
- // Set the readConcern on the OperationContext.
- repl::ReadConcernArgs readConcernArgs;
- ASSERT_OK(readConcernArgs.initialize(BSON("find"
- << "test"
- << repl::ReadConcernArgs::kReadConcernFieldName
- << BSON(repl::ReadConcernArgs::kLevelFieldName
- << "snapshot"))));
- repl::ReadConcernArgs::get(opCtx()) = readConcernArgs;
-
- // Perform initial unstash, which sets up a WriteUnitOfWork.
- OperationContextSession::get(opCtx())->unstashTransactionResources(opCtx(), "find");
- ASSERT_EQUALS(originalLocker, opCtx()->lockState());
- ASSERT_EQUALS(originalRecoveryUnit, opCtx()->recoveryUnit());
- ASSERT(opCtx()->getWriteUnitOfWork());
-
- {
- // Make it look like we're in a DBDirectClient running a nested operation.
- DirectClientSetter inDirectClient(opCtx());
- OperationContextSession innerScopedSession(
- opCtx(), true, boost::none, boost::none, "testDB", "find");
-
- OperationContextSession::get(opCtx())->stashTransactionResources(opCtx());
-
- // The stash was a noop, so the locker, RecoveryUnit, and WriteUnitOfWork on the
- // OperationContext are unaffected.
- ASSERT_EQUALS(originalLocker, opCtx()->lockState());
- ASSERT_EQUALS(originalRecoveryUnit, opCtx()->recoveryUnit());
- ASSERT(opCtx()->getWriteUnitOfWork());
- }
- }
-}
-
-TEST_F(SessionCatalogTest, UnstashInNestedSessionIsANoop) {
- opCtx()->setLogicalSessionId(makeLogicalSessionIdForTest());
- opCtx()->setTxnNumber(1);
-
- {
- OperationContextSession outerScopedSession(
- opCtx(), true, /* autocommit */ false, /* startTransaction */ true, "testDB", "find");
-
- Locker* originalLocker = opCtx()->lockState();
- RecoveryUnit* originalRecoveryUnit = opCtx()->recoveryUnit();
- ASSERT(originalLocker);
- ASSERT(originalRecoveryUnit);
-
- // Set the readConcern on the OperationContext.
- repl::ReadConcernArgs readConcernArgs;
- ASSERT_OK(readConcernArgs.initialize(BSON("find"
- << "test"
- << repl::ReadConcernArgs::kReadConcernFieldName
- << BSON(repl::ReadConcernArgs::kLevelFieldName
- << "snapshot"))));
- repl::ReadConcernArgs::get(opCtx()) = readConcernArgs;
-
- {
- // Make it look like we're in a DBDirectClient running a nested operation.
- DirectClientSetter inDirectClient(opCtx());
- OperationContextSession innerScopedSession(
- opCtx(), true, boost::none, boost::none, "testDB", "find");
-
- OperationContextSession::get(opCtx())->unstashTransactionResources(opCtx(), "find");
-
- // The unstash was a noop, so the OperationContext did not get a WriteUnitOfWork.
- ASSERT_EQUALS(originalLocker, opCtx()->lockState());
- ASSERT_EQUALS(originalRecoveryUnit, opCtx()->recoveryUnit());
- ASSERT_FALSE(opCtx()->getWriteUnitOfWork());
- }
- }
-}
-
TEST_F(SessionCatalogTest, ScanSessions) {
std::vector<LogicalSessionId> lsids;
auto workerFn = [&](OperationContext* opCtx, Session* session) {
diff --git a/src/mongo/db/session_test.cpp b/src/mongo/db/session_test.cpp
index a9e1a792b2d..e32c60f9d1b 100644
--- a/src/mongo/db/session_test.cpp
+++ b/src/mongo/db/session_test.cpp
@@ -182,47 +182,6 @@ protected:
OplogSlot());
}
- void runFunctionFromDifferentOpCtx(std::function<void(OperationContext*)> func) {
- // Stash the original client.
- auto originalClient = Client::releaseCurrent();
-
- // Create a new client (e.g. for migration) and opCtx.
- auto service = opCtx()->getServiceContext();
- auto newClientOwned = service->makeClient("newClient");
- auto newClient = newClientOwned.get();
- Client::setCurrent(std::move(newClientOwned));
- auto newOpCtx = newClient->makeOperationContext();
-
- // Run the function on bahalf of another operation context.
- func(newOpCtx.get());
-
- // Restore the original client.
- newOpCtx.reset();
- Client::releaseCurrent();
- Client::setCurrent(std::move(originalClient));
- }
-
- void bumpTxnNumberFromDifferentOpCtx(Session* session, TxnNumber newTxnNum) {
- auto func = [session, newTxnNum](OperationContext* opCtx) {
- // Check that there is a transaction in progress with a lower txnNumber.
- ASSERT(session->inMultiDocumentTransaction());
- ASSERT_LT(session->getActiveTxnNumberForTest(), newTxnNum);
-
- // Check that the transaction has some operations, so we can ensure they are cleared.
- ASSERT_GT(session->transactionOperationsForTest().size(), 0u);
-
- // Bump the active transaction number on the session. This should clear all state from
- // the previous transaction.
- session->beginOrContinueTxnOnMigration(opCtx, newTxnNum);
- ASSERT_EQ(session->getActiveTxnNumberForTest(), newTxnNum);
- ASSERT_FALSE(session->inMultiDocumentTransaction());
- ASSERT_FALSE(session->transactionIsAborted());
- ASSERT_EQ(session->transactionOperationsForTest().size(), 0u);
- };
-
- runFunctionFromDifferentOpCtx(func);
- }
-
OpObserverMock* _opObserver = nullptr;
};
@@ -232,7 +191,7 @@ TEST_F(SessionTest, SessionEntryNotWrittenOnBegin) {
session.refreshFromStorageIfNeeded(opCtx());
const TxnNumber txnNum = 20;
- session.beginOrContinueTxn(opCtx(), txnNum, boost::none, boost::none, "testDB", "insert");
+ session.beginOrContinueTxn(opCtx(), txnNum);
ASSERT_EQ(sessionId, session.getSessionId());
ASSERT(session.getLastWriteOpTime(txnNum).isNull());
@@ -250,7 +209,7 @@ TEST_F(SessionTest, SessionEntryWrittenAtFirstWrite) {
session.refreshFromStorageIfNeeded(opCtx());
const TxnNumber txnNum = 21;
- session.beginOrContinueTxn(opCtx(), txnNum, boost::none, boost::none, "testDB", "insert");
+ session.beginOrContinueTxn(opCtx(), txnNum);
const auto opTime = [&] {
AutoGetCollection autoColl(opCtx(), kNss, MODE_IX);
@@ -283,7 +242,7 @@ TEST_F(SessionTest, StartingNewerTransactionUpdatesThePersistedSession) {
session.refreshFromStorageIfNeeded(opCtx());
const auto writeTxnRecordFn = [&](TxnNumber txnNum, StmtId stmtId, repl::OpTime prevOpTime) {
- session.beginOrContinueTxn(opCtx(), txnNum, boost::none, boost::none, "testDB", "insert");
+ session.beginOrContinueTxn(opCtx(), txnNum);
AutoGetCollection autoColl(opCtx(), kNss, MODE_IX);
WriteUnitOfWork wuow(opCtx());
@@ -322,10 +281,9 @@ TEST_F(SessionTest, StartingOldTxnShouldAssert) {
session.refreshFromStorageIfNeeded(opCtx());
const TxnNumber txnNum = 20;
- session.beginOrContinueTxn(opCtx(), txnNum, boost::none, boost::none, "testDB", "insert");
+ session.beginOrContinueTxn(opCtx(), txnNum);
- ASSERT_THROWS_CODE(session.beginOrContinueTxn(
- opCtx(), txnNum - 1, boost::none, boost::none, "testDB", "insert"),
+ ASSERT_THROWS_CODE(session.beginOrContinueTxn(opCtx(), txnNum - 1),
AssertionException,
ErrorCodes::TransactionTooOld);
ASSERT(session.getLastWriteOpTime(txnNum).isNull());
@@ -343,7 +301,7 @@ TEST_F(SessionTest, SessionTransactionsCollectionNotDefaultCreated) {
ASSERT(client.runCommand(nss.db().toString(), BSON("drop" << nss.coll()), dropResult));
const TxnNumber txnNum = 21;
- session.beginOrContinueTxn(opCtx(), txnNum, boost::none, boost::none, "testDB", "insert");
+ session.beginOrContinueTxn(opCtx(), txnNum);
AutoGetCollection autoColl(opCtx(), kNss, MODE_IX);
WriteUnitOfWork wuow(opCtx());
@@ -358,7 +316,7 @@ TEST_F(SessionTest, CheckStatementExecuted) {
session.refreshFromStorageIfNeeded(opCtx());
const TxnNumber txnNum = 100;
- session.beginOrContinueTxn(opCtx(), txnNum, boost::none, boost::none, "testDB", "insert");
+ session.beginOrContinueTxn(opCtx(), txnNum);
const auto writeTxnRecordFn = [&](StmtId stmtId, repl::OpTime prevOpTime) {
AutoGetCollection autoColl(opCtx(), kNss, MODE_IX);
@@ -399,7 +357,7 @@ TEST_F(SessionTest, CheckStatementExecutedForOldTransactionThrows) {
session.refreshFromStorageIfNeeded(opCtx());
const TxnNumber txnNum = 100;
- session.beginOrContinueTxn(opCtx(), txnNum, boost::none, boost::none, "testDB", "insert");
+ session.beginOrContinueTxn(opCtx(), txnNum);
ASSERT_THROWS_CODE(session.checkStatementExecuted(opCtx(), txnNum - 1, 0),
AssertionException,
@@ -422,7 +380,7 @@ TEST_F(SessionTest, WriteOpCompletedOnPrimaryForOldTransactionThrows) {
session.refreshFromStorageIfNeeded(opCtx());
const TxnNumber txnNum = 100;
- session.beginOrContinueTxn(opCtx(), txnNum, boost::none, boost::none, "testDB", "insert");
+ session.beginOrContinueTxn(opCtx(), txnNum);
{
AutoGetCollection autoColl(opCtx(), kNss, MODE_IX);
@@ -449,7 +407,7 @@ TEST_F(SessionTest, WriteOpCompletedOnPrimaryForInvalidatedTransactionThrows) {
session.refreshFromStorageIfNeeded(opCtx());
const TxnNumber txnNum = 100;
- session.beginOrContinueTxn(opCtx(), txnNum, boost::none, boost::none, "testDB", "insert");
+ session.beginOrContinueTxn(opCtx(), txnNum);
AutoGetCollection autoColl(opCtx(), kNss, MODE_IX);
WriteUnitOfWork wuow(opCtx());
@@ -469,7 +427,7 @@ TEST_F(SessionTest, WriteOpCompletedOnPrimaryCommitIgnoresInvalidation) {
session.refreshFromStorageIfNeeded(opCtx());
const TxnNumber txnNum = 100;
- session.beginOrContinueTxn(opCtx(), txnNum, boost::none, boost::none, "testDB", "insert");
+ session.beginOrContinueTxn(opCtx(), txnNum);
{
AutoGetCollection autoColl(opCtx(), kNss, MODE_IX);
@@ -564,7 +522,7 @@ TEST_F(SessionTest, ErrorOnlyWhenStmtIdBeingCheckedIsNotInCache) {
Session session(sessionId);
session.refreshFromStorageIfNeeded(opCtx());
- session.beginOrContinueTxn(opCtx(), txnNum, boost::none, boost::none, "testDB", "insert");
+ session.beginOrContinueTxn(opCtx(), txnNum);
auto firstOpTime = ([&]() {
AutoGetCollection autoColl(opCtx(), kNss, MODE_IX);
@@ -640,2572 +598,5 @@ TEST_F(SessionTest, ErrorOnlyWhenStmtIdBeingCheckedIsNotInCache) {
ASSERT_THROWS(session.checkStatementExecuted(opCtx(), txnNum, 2), AssertionException);
}
-// Test that transaction lock acquisition times out in `maxTransactionLockRequestTimeoutMillis`
-// milliseconds.
-TEST_F(SessionTest, TransactionThrowsLockTimeoutIfLockIsUnavailable) {
- const std::string dbName = "TestDB";
-
- /**
- * Set up a transaction, take a database exclusive lock and then stash the transaction and
- * Client.
- */
-
- const auto sessionId = makeLogicalSessionIdForTest();
- Session session(sessionId);
- session.refreshFromStorageIfNeeded(opCtx());
-
- const TxnNumber txnNum = 20;
- opCtx()->setLogicalSessionId(sessionId);
- opCtx()->setTxnNumber(txnNum);
- session.beginOrContinueTxn(opCtx(), txnNum, false, true, "testDB", "insert");
- session.unstashTransactionResources(opCtx(), "insert");
-
- { Lock::DBLock dbXLock(opCtx(), dbName, MODE_X); }
- session.stashTransactionResources(opCtx());
- auto clientWithDatabaseXLock = Client::releaseCurrent();
-
- /**
- * Make a new Session, Client, OperationContext and transaction and then attempt to take the
- * same database exclusive lock, which should conflict because the other transaction already
- * took it.
- */
-
- auto service = opCtx()->getServiceContext();
- auto newClientOwned = service->makeClient("newTransactionClient");
- auto newClient = newClientOwned.get();
- Client::setCurrent(std::move(newClientOwned));
- auto newOpCtx = newClient->makeOperationContext();
-
- const auto newSessionId = makeLogicalSessionIdForTest();
- Session newSession(newSessionId);
- newSession.refreshFromStorageIfNeeded(newOpCtx.get());
-
- const TxnNumber newTxnNum = 10;
- newOpCtx.get()->setLogicalSessionId(newSessionId);
- newOpCtx.get()->setTxnNumber(newTxnNum);
- newSession.beginOrContinueTxn(newOpCtx.get(), newTxnNum, false, true, "testDB", "insert");
- newSession.unstashTransactionResources(newOpCtx.get(), "insert");
-
- Date_t t1 = Date_t::now();
- ASSERT_THROWS_CODE(
- Lock::DBLock(newOpCtx.get(), dbName, MODE_X), AssertionException, ErrorCodes::LockTimeout);
- Date_t t2 = Date_t::now();
- int defaultMaxTransactionLockRequestTimeoutMillis = 5;
- ASSERT_GTE(t2 - t1, Milliseconds(defaultMaxTransactionLockRequestTimeoutMillis));
-
- // A non-conflicting lock acquisition should work just fine.
- { Lock::DBLock(newOpCtx.get(), "NewTestDB", MODE_X); }
-
- // Restore the original client so that teardown works.
- newOpCtx.reset();
- Client::releaseCurrent();
- Client::setCurrent(std::move(clientWithDatabaseXLock));
-}
-
-TEST_F(SessionTest, StashAndUnstashResources) {
- const auto sessionId = makeLogicalSessionIdForTest();
- const TxnNumber txnNum = 20;
- opCtx()->setLogicalSessionId(sessionId);
- opCtx()->setTxnNumber(txnNum);
-
- Locker* originalLocker = opCtx()->lockState();
- RecoveryUnit* originalRecoveryUnit = opCtx()->recoveryUnit();
- ASSERT(originalLocker);
- ASSERT(originalRecoveryUnit);
-
- Session session(sessionId);
- session.refreshFromStorageIfNeeded(opCtx());
-
- session.beginOrContinueTxn(opCtx(), txnNum, false, true, "testDB", "find");
-
- repl::ReadConcernArgs readConcernArgs;
- ASSERT_OK(readConcernArgs.initialize(BSON("find"
- << "test"
- << repl::ReadConcernArgs::kReadConcernFieldName
- << BSON(repl::ReadConcernArgs::kLevelFieldName
- << "snapshot"))));
- repl::ReadConcernArgs::get(opCtx()) = readConcernArgs;
-
- // Perform initial unstash which sets up a WriteUnitOfWork.
- session.unstashTransactionResources(opCtx(), "find");
- ASSERT_EQUALS(originalLocker, opCtx()->lockState());
- ASSERT_EQUALS(originalRecoveryUnit, opCtx()->recoveryUnit());
- ASSERT(opCtx()->getWriteUnitOfWork());
- ASSERT(opCtx()->lockState()->isLocked());
-
- // Stash resources. The original Locker and RecoveryUnit now belong to the stash.
- session.stashTransactionResources(opCtx());
- ASSERT_NOT_EQUALS(originalLocker, opCtx()->lockState());
- ASSERT_NOT_EQUALS(originalRecoveryUnit, opCtx()->recoveryUnit());
- ASSERT(!opCtx()->getWriteUnitOfWork());
-
- // Unset the read concern on the OperationContext. This is needed to unstash.
- repl::ReadConcernArgs::get(opCtx()) = repl::ReadConcernArgs();
-
- // Unstash the stashed resources. This restores the original Locker and RecoveryUnit to the
- // OperationContext.
- session.unstashTransactionResources(opCtx(), "find");
- ASSERT_EQUALS(originalLocker, opCtx()->lockState());
- ASSERT_EQUALS(originalRecoveryUnit, opCtx()->recoveryUnit());
- ASSERT(opCtx()->getWriteUnitOfWork());
-
- // Commit the transaction. This allows us to release locks.
- session.commitUnpreparedTransaction(opCtx());
-}
-
-TEST_F(SessionTest, ReportStashedResources) {
- Date_t startTime = Date_t::now();
- const auto sessionId = makeLogicalSessionIdForTest();
- const TxnNumber txnNum = 20;
- const bool autocommit = false;
- opCtx()->setLogicalSessionId(sessionId);
- opCtx()->setTxnNumber(txnNum);
-
- ASSERT(opCtx()->lockState());
- ASSERT(opCtx()->recoveryUnit());
-
- Session session(sessionId);
- session.refreshFromStorageIfNeeded(opCtx());
-
- // Create a ClientMetadata object and set it on ClientMetadataIsMasterState.
- BSONObjBuilder builder;
- ASSERT_OK(ClientMetadata::serializePrivate("driverName",
- "driverVersion",
- "osType",
- "osName",
- "osArchitecture",
- "osVersion",
- "appName",
- &builder));
- auto obj = builder.obj();
- auto clientMetadata = ClientMetadata::parse(obj["client"]);
- auto& clientMetadataIsMasterState = ClientMetadataIsMasterState::get(opCtx()->getClient());
- clientMetadataIsMasterState.setClientMetadata(opCtx()->getClient(),
- std::move(clientMetadata.getValue()));
-
- session.beginOrContinueTxn(opCtx(), txnNum, autocommit, true, "testDB", "find");
-
- repl::ReadConcernArgs readConcernArgs;
- ASSERT_OK(readConcernArgs.initialize(BSON("find"
- << "test"
- << repl::ReadConcernArgs::kReadConcernFieldName
- << BSON(repl::ReadConcernArgs::kLevelFieldName
- << "snapshot"))));
- repl::ReadConcernArgs::get(opCtx()) = readConcernArgs;
-
- // Perform initial unstash which sets up a WriteUnitOfWork.
- session.unstashTransactionResources(opCtx(), "find");
- ASSERT(opCtx()->getWriteUnitOfWork());
- ASSERT(opCtx()->lockState()->isLocked());
-
- // Stash resources. The original Locker and RecoveryUnit now belong to the stash.
- session.stashTransactionResources(opCtx());
- ASSERT(!opCtx()->getWriteUnitOfWork());
-
- // Verify that the Session's report of its own stashed state aligns with our expectations.
- auto stashedState = session.reportStashedState();
- auto transactionDocument = stashedState.getObjectField("transaction");
- auto parametersDocument = transactionDocument.getObjectField("parameters");
-
- ASSERT_EQ(stashedState.getField("host").valueStringData().toString(),
- getHostNameCachedAndPort());
- ASSERT_EQ(stashedState.getField("desc").valueStringData().toString(), "inactive transaction");
- ASSERT_BSONOBJ_EQ(stashedState.getField("lsid").Obj(), sessionId.toBSON());
- ASSERT_EQ(parametersDocument.getField("txnNumber").numberLong(), txnNum);
- ASSERT_EQ(parametersDocument.getField("autocommit").boolean(), autocommit);
- ASSERT_BSONELT_EQ(parametersDocument.getField("readConcern"),
- readConcernArgs.toBSON().getField("readConcern"));
- ASSERT_GTE(transactionDocument.getField("readTimestamp").timestamp(), Timestamp(0, 0));
- ASSERT_GTE(
- dateFromISOString(transactionDocument.getField("startWallClockTime").valueStringData())
- .getValue(),
- startTime);
- ASSERT_EQ(
- dateFromISOString(transactionDocument.getField("expiryTime").valueStringData()).getValue(),
- Date_t::fromMillisSinceEpoch(session.getSingleTransactionStats()->getStartTime() / 1000) +
- stdx::chrono::seconds{transactionLifetimeLimitSeconds.load()});
- ASSERT_EQ(stashedState.getField("client").valueStringData().toString(), "");
- ASSERT_EQ(stashedState.getField("connectionId").numberLong(), 0);
- ASSERT_EQ(stashedState.getField("appName").valueStringData().toString(), "appName");
- ASSERT_BSONOBJ_EQ(stashedState.getField("clientMetadata").Obj(), obj.getField("client").Obj());
- ASSERT_EQ(stashedState.getField("waitingForLock").boolean(), false);
- ASSERT_EQ(stashedState.getField("active").boolean(), false);
- // For the following time metrics, we are only verifying that the transaction sub-document is
- // being constructed correctly with proper types because we have other tests to verify that the
- // values are being tracked correctly.
- ASSERT_GTE(transactionDocument.getField("timeOpenMicros").numberLong(), 0);
- ASSERT_GTE(transactionDocument.getField("timeActiveMicros").numberLong(), 0);
- ASSERT_GTE(transactionDocument.getField("timeInactiveMicros").numberLong(), 0);
-
- // Unset the read concern on the OperationContext. This is needed to unstash.
- repl::ReadConcernArgs::get(opCtx()) = repl::ReadConcernArgs();
-
- // Unstash the stashed resources. This restores the original Locker and RecoveryUnit to the
- // OperationContext.
- session.unstashTransactionResources(opCtx(), "commitTransaction");
- ASSERT(opCtx()->getWriteUnitOfWork());
-
- // With the resources unstashed, verify that the Session reports an empty stashed state.
- ASSERT(session.reportStashedState().isEmpty());
-
- // Commit the transaction. This allows us to release locks.
- session.commitUnpreparedTransaction(opCtx());
-}
-
-TEST_F(SessionTest, ReportUnstashedResources) {
- Date_t startTime = Date_t::now();
- const auto sessionId = makeLogicalSessionIdForTest();
- const TxnNumber txnNum = 20;
- const bool autocommit = false;
- opCtx()->setLogicalSessionId(sessionId);
- opCtx()->setTxnNumber(txnNum);
-
- ASSERT(opCtx()->lockState());
- ASSERT(opCtx()->recoveryUnit());
-
- Session session(sessionId);
- session.refreshFromStorageIfNeeded(opCtx());
-
- session.beginOrContinueTxn(opCtx(), txnNum, autocommit, true, "testDB", "find");
-
- repl::ReadConcernArgs readConcernArgs;
- ASSERT_OK(readConcernArgs.initialize(BSON("find"
- << "test"
- << repl::ReadConcernArgs::kReadConcernFieldName
- << BSON(repl::ReadConcernArgs::kLevelFieldName
- << "snapshot"))));
- repl::ReadConcernArgs::get(opCtx()) = readConcernArgs;
-
- // Perform initial unstash which sets up a WriteUnitOfWork.
- session.unstashTransactionResources(opCtx(), "find");
- ASSERT(opCtx()->getWriteUnitOfWork());
- ASSERT(opCtx()->lockState()->isLocked());
-
- // Verify that the Session's report of its own unstashed state aligns with our expectations.
- BSONObjBuilder unstashedStateBuilder;
- session.reportUnstashedState(repl::ReadConcernArgs::get(opCtx()), &unstashedStateBuilder);
- auto unstashedState = unstashedStateBuilder.obj();
- auto transactionDocument = unstashedState.getObjectField("transaction");
- auto parametersDocument = transactionDocument.getObjectField("parameters");
-
- ASSERT_EQ(parametersDocument.getField("txnNumber").numberLong(), txnNum);
- ASSERT_EQ(parametersDocument.getField("autocommit").boolean(), autocommit);
- ASSERT_BSONELT_EQ(parametersDocument.getField("readConcern"),
- readConcernArgs.toBSON().getField("readConcern"));
- ASSERT_GTE(transactionDocument.getField("readTimestamp").timestamp(), Timestamp(0, 0));
- ASSERT_GTE(
- dateFromISOString(transactionDocument.getField("startWallClockTime").valueStringData())
- .getValue(),
- startTime);
- ASSERT_EQ(
- dateFromISOString(transactionDocument.getField("expiryTime").valueStringData()).getValue(),
- Date_t::fromMillisSinceEpoch(session.getSingleTransactionStats()->getStartTime() / 1000) +
- stdx::chrono::seconds{transactionLifetimeLimitSeconds.load()});
- // For the following time metrics, we are only verifying that the transaction sub-document is
- // being constructed correctly with proper types because we have other tests to verify that the
- // values are being tracked correctly.
- ASSERT_GTE(transactionDocument.getField("timeOpenMicros").numberLong(), 0);
- ASSERT_GTE(transactionDocument.getField("timeActiveMicros").numberLong(), 0);
- ASSERT_GTE(transactionDocument.getField("timeInactiveMicros").numberLong(), 0);
-
- // Stash resources. The original Locker and RecoveryUnit now belong to the stash.
- session.stashTransactionResources(opCtx());
- ASSERT(!opCtx()->getWriteUnitOfWork());
-
- // With the resources stashed, verify that the Session reports an empty unstashed state.
- BSONObjBuilder builder;
- session.reportUnstashedState(repl::ReadConcernArgs::get(opCtx()), &builder);
- ASSERT(builder.obj().isEmpty());
-}
-
-TEST_F(SessionTest, ReportUnstashedResourcesForARetryableWrite) {
- const auto sessionId = makeLogicalSessionIdForTest();
- const TxnNumber txnNum = 20;
- opCtx()->setLogicalSessionId(sessionId);
- opCtx()->setTxnNumber(txnNum);
-
- ASSERT(opCtx()->lockState());
- ASSERT(opCtx()->recoveryUnit());
-
- Session session(sessionId);
- session.refreshFromStorageIfNeeded(opCtx());
-
- session.beginOrContinueTxn(opCtx(), txnNum, boost::none, boost::none, "testDB", "find");
- session.unstashTransactionResources(opCtx(), "find");
-
- // Build a BSONObj containing the details which we expect to see reported when we call
- // Session::reportUnstashedState. For a retryable write, we should only include the txnNumber.
- BSONObjBuilder reportBuilder;
- BSONObjBuilder transactionBuilder(reportBuilder.subobjStart("transaction"));
- BSONObjBuilder parametersBuilder(transactionBuilder.subobjStart("parameters"));
- parametersBuilder.append("txnNumber", txnNum);
- parametersBuilder.done();
- transactionBuilder.done();
-
- // Verify that the Session's report of its own unstashed state aligns with our expectations.
- BSONObjBuilder unstashedStateBuilder;
- session.reportUnstashedState(repl::ReadConcernArgs::get(opCtx()), &unstashedStateBuilder);
- ASSERT_BSONOBJ_EQ(unstashedStateBuilder.obj(), reportBuilder.obj());
-}
-
-TEST_F(SessionTest, CannotSpecifyStartTransactionOnInProgressTxn) {
- const auto sessionId = makeLogicalSessionIdForTest();
- Session session(sessionId);
- session.refreshFromStorageIfNeeded(opCtx());
-
- // Autocommit should be true by default
- ASSERT(session.getAutocommit());
-
- const TxnNumber txnNum = 100;
- // Must specify startTransaction=true and autocommit=false to start a transaction.
- session.beginOrContinueTxn(opCtx(), txnNum, false, true, "testDB", "insert");
-
- // Autocommit should be set to false and we should be in a mult-doc transaction.
- ASSERT_FALSE(session.getAutocommit());
- ASSERT_TRUE(session.inMultiDocumentTransaction());
-
- // Cannot try to start a transaction that already started.
- ASSERT_THROWS_CODE(session.beginOrContinueTxn(opCtx(), txnNum, false, true, "testDB", "insert"),
- AssertionException,
- ErrorCodes::ConflictingOperationInProgress);
-}
-
-TEST_F(SessionTest, AutocommitRequiredOnEveryTxnOp) {
- const auto sessionId = makeLogicalSessionIdForTest();
- Session session(sessionId);
- session.refreshFromStorageIfNeeded(opCtx());
-
- // Autocommit should be true by default
- ASSERT(session.getAutocommit());
-
- const TxnNumber txnNum = 100;
- opCtx()->setLogicalSessionId(sessionId);
- opCtx()->setTxnNumber(txnNum);
- session.beginOrContinueTxn(opCtx(), txnNum, false, true, "testDB", "insert");
-
- // We must have stashed transaction resources to do a second operation on the transaction.
- session.unstashTransactionResources(opCtx(), "insert");
- // The transaction machinery cannot store an empty locker.
- { Lock::GlobalLock lk(opCtx(), MODE_IX, Date_t::now(), Lock::InterruptBehavior::kThrow); }
- session.stashTransactionResources(opCtx());
-
- // Autocommit should be set to false
- ASSERT_FALSE(session.getAutocommit());
-
- // Omitting 'autocommit' after the first statement of a transaction should throw an error.
- ASSERT_THROWS_CODE(
- session.beginOrContinueTxn(opCtx(), txnNum, boost::none, boost::none, "testDB", "insert"),
- AssertionException,
- ErrorCodes::InvalidOptions);
-
- // Setting 'autocommit=true' should throw an error.
- ASSERT_THROWS_CODE(
- session.beginOrContinueTxn(opCtx(), txnNum, true, boost::none, "testDB", "insert"),
- AssertionException,
- ErrorCodes::InvalidOptions);
-
- // Including autocommit=false should succeed.
- session.beginOrContinueTxn(opCtx(), txnNum, false, boost::none, "testDB", "insert");
-}
-
-TEST_F(SessionTest, SameTransactionPreservesStoredStatements) {
- const auto sessionId = makeLogicalSessionIdForTest();
- Session session(sessionId);
- session.refreshFromStorageIfNeeded(opCtx());
-
- const TxnNumber txnNum = 22;
- opCtx()->setLogicalSessionId(sessionId);
- opCtx()->setTxnNumber(txnNum);
- session.beginOrContinueTxn(opCtx(), txnNum, false, true, "testDB", "insert");
-
- // We must have stashed transaction resources to re-open the transaction.
- session.unstashTransactionResources(opCtx(), "insert");
- auto operation = repl::OplogEntry::makeInsertOperation(kNss, kUUID, BSON("TestValue" << 0));
- session.addTransactionOperation(opCtx(), operation);
- ASSERT_BSONOBJ_EQ(operation.toBSON(), session.transactionOperationsForTest()[0].toBSON());
- // The transaction machinery cannot store an empty locker.
- { Lock::GlobalLock lk(opCtx(), MODE_IX, Date_t::now(), Lock::InterruptBehavior::kThrow); }
- session.stashTransactionResources(opCtx());
-
- // Check the transaction operations before re-opening the transaction.
- ASSERT_BSONOBJ_EQ(operation.toBSON(), session.transactionOperationsForTest()[0].toBSON());
-
- // Re-opening the same transaction should have no effect.
- session.beginOrContinueTxn(opCtx(), txnNum, false, boost::none, "testDB", "insert");
- ASSERT_BSONOBJ_EQ(operation.toBSON(), session.transactionOperationsForTest()[0].toBSON());
-}
-
-TEST_F(SessionTest, AbortClearsStoredStatements) {
- const auto sessionId = makeLogicalSessionIdForTest();
- Session session(sessionId);
- session.refreshFromStorageIfNeeded(opCtx());
-
- const TxnNumber txnNum = 24;
- opCtx()->setLogicalSessionId(sessionId);
- opCtx()->setTxnNumber(txnNum);
- session.beginOrContinueTxn(opCtx(), txnNum, false, true, "testDB", "insert");
- session.unstashTransactionResources(opCtx(), "insert");
- auto operation = repl::OplogEntry::makeInsertOperation(kNss, kUUID, BSON("TestValue" << 0));
- session.addTransactionOperation(opCtx(), operation);
- ASSERT_BSONOBJ_EQ(operation.toBSON(), session.transactionOperationsForTest()[0].toBSON());
- // The transaction machinery cannot store an empty locker.
- { Lock::GlobalLock lk(opCtx(), MODE_IX, Date_t::now(), Lock::InterruptBehavior::kThrow); }
- session.stashTransactionResources(opCtx());
- session.abortArbitraryTransaction();
- ASSERT_TRUE(session.transactionOperationsForTest().empty());
- ASSERT_TRUE(session.transactionIsAborted());
-}
-
-// This test makes sure the commit machinery works even when no operations are done on the
-// transaction.
-TEST_F(SessionTest, EmptyTransactionCommit) {
- const auto sessionId = makeLogicalSessionIdForTest();
- Session session(sessionId);
- session.refreshFromStorageIfNeeded(opCtx());
-
- const TxnNumber txnNum = 25;
- opCtx()->setLogicalSessionId(sessionId);
- opCtx()->setTxnNumber(txnNum);
- session.beginOrContinueTxn(opCtx(), txnNum, false, true, "admin", "commitTransaction");
- session.unstashTransactionResources(opCtx(), "commitTransaction");
- // The transaction machinery cannot store an empty locker.
- Lock::GlobalLock lk(opCtx(), MODE_IX, Date_t::now(), Lock::InterruptBehavior::kThrow);
- session.commitUnpreparedTransaction(opCtx());
- session.stashTransactionResources(opCtx());
- ASSERT_TRUE(session.transactionIsCommitted());
-}
-
-TEST_F(SessionTest, CommitTransactionSetsCommitTimestampOnPreparedTransaction) {
- const auto sessionId = makeLogicalSessionIdForTest();
- Session session(sessionId);
- session.refreshFromStorageIfNeeded(opCtx());
-
- Timestamp actualCommitTimestamp;
- auto originalFn = _opObserver->onTransactionCommitFn;
- _opObserver->onTransactionCommitFn = [&](bool wasPrepared) {
- originalFn(wasPrepared);
- ASSERT(wasPrepared);
- actualCommitTimestamp = opCtx()->recoveryUnit()->getCommitTimestamp();
- };
-
- const auto commitTimestamp = Timestamp(6, 6);
- const TxnNumber txnNum = 25;
- opCtx()->setLogicalSessionId(sessionId);
- opCtx()->setTxnNumber(txnNum);
- session.beginOrContinueTxn(opCtx(), txnNum, false, true, "admin", "commitTransaction");
- session.unstashTransactionResources(opCtx(), "commitTransaction");
- // The transaction machinery cannot store an empty locker.
- Lock::GlobalLock lk(opCtx(), MODE_IX, Date_t::now(), Lock::InterruptBehavior::kThrow);
- session.prepareTransaction(opCtx());
- session.commitPreparedTransaction(opCtx(), commitTimestamp);
-
- ASSERT_EQ(commitTimestamp, actualCommitTimestamp);
- // The recovery unit is reset on commit.
- ASSERT(opCtx()->recoveryUnit()->getCommitTimestamp().isNull());
-
- session.stashTransactionResources(opCtx());
- ASSERT_TRUE(session.transactionIsCommitted());
- ASSERT(opCtx()->recoveryUnit()->getCommitTimestamp().isNull());
-}
-
-TEST_F(SessionTest, CommitTransactionWithCommitTimestampFailsOnUnpreparedTransaction) {
- const auto sessionId = makeLogicalSessionIdForTest();
- Session session(sessionId);
- session.refreshFromStorageIfNeeded(opCtx());
-
- const auto commitTimestamp = Timestamp(6, 6);
- const TxnNumber txnNum = 25;
- opCtx()->setLogicalSessionId(sessionId);
- opCtx()->setTxnNumber(txnNum);
- session.beginOrContinueTxn(opCtx(), txnNum, false, true, "admin", "commitTransaction");
- session.unstashTransactionResources(opCtx(), "commitTransaction");
- // The transaction machinery cannot store an empty locker.
- Lock::GlobalLock lk(opCtx(), MODE_IX, Date_t::now(), Lock::InterruptBehavior::kThrow);
- ASSERT_THROWS_CODE(session.commitPreparedTransaction(opCtx(), commitTimestamp),
- AssertionException,
- ErrorCodes::InvalidOptions);
-}
-
-TEST_F(SessionTest, CommitTransactionDoesNotSetCommitTimestampOnUnpreparedTransaction) {
- const auto sessionId = makeLogicalSessionIdForTest();
- Session session(sessionId);
- session.refreshFromStorageIfNeeded(opCtx());
-
- Timestamp actualCommitTimestamp;
- auto originalFn = _opObserver->onTransactionCommitFn;
- _opObserver->onTransactionCommitFn = [&](bool wasPrepared) {
- originalFn(wasPrepared);
- ASSERT_FALSE(wasPrepared);
- actualCommitTimestamp = opCtx()->recoveryUnit()->getCommitTimestamp();
- };
-
- const TxnNumber txnNum = 25;
- opCtx()->setLogicalSessionId(sessionId);
- opCtx()->setTxnNumber(txnNum);
- session.beginOrContinueTxn(opCtx(), txnNum, false, true, "admin", "commitTransaction");
- session.unstashTransactionResources(opCtx(), "commitTransaction");
- // The transaction machinery cannot store an empty locker.
- Lock::GlobalLock lk(opCtx(), MODE_IX, Date_t::now(), Lock::InterruptBehavior::kThrow);
- session.commitUnpreparedTransaction(opCtx());
-
- ASSERT(opCtx()->recoveryUnit()->getCommitTimestamp().isNull());
- ASSERT(actualCommitTimestamp.isNull());
-
- session.stashTransactionResources(opCtx());
- ASSERT_TRUE(session.transactionIsCommitted());
- ASSERT(opCtx()->recoveryUnit()->getCommitTimestamp().isNull());
-}
-
-TEST_F(SessionTest, CommitTransactionWithoutCommitTimestampFailsOnPreparedTransaction) {
- const auto sessionId = makeLogicalSessionIdForTest();
- Session session(sessionId);
- session.refreshFromStorageIfNeeded(opCtx());
-
- const TxnNumber txnNum = 25;
- opCtx()->setLogicalSessionId(sessionId);
- opCtx()->setTxnNumber(txnNum);
- session.beginOrContinueTxn(opCtx(), txnNum, false, true, "admin", "commitTransaction");
- session.unstashTransactionResources(opCtx(), "commitTransaction");
- // The transaction machinery cannot store an empty locker.
- Lock::GlobalLock lk(opCtx(), MODE_IX, Date_t::now(), Lock::InterruptBehavior::kThrow);
- session.prepareTransaction(opCtx());
- ASSERT_THROWS_CODE(session.commitUnpreparedTransaction(opCtx()),
- AssertionException,
- ErrorCodes::InvalidOptions);
-}
-
-TEST_F(SessionTest, CommitTransactionWithNullCommitTimestampFailsOnPreparedTransaction) {
- const auto sessionId = makeLogicalSessionIdForTest();
- Session session(sessionId);
- session.refreshFromStorageIfNeeded(opCtx());
-
- const TxnNumber txnNum = 25;
- opCtx()->setLogicalSessionId(sessionId);
- opCtx()->setTxnNumber(txnNum);
- session.beginOrContinueTxn(opCtx(), txnNum, false, true, "admin", "commitTransaction");
- session.unstashTransactionResources(opCtx(), "commitTransaction");
- // The transaction machinery cannot store an empty locker.
- Lock::GlobalLock lk(opCtx(), MODE_IX, Date_t::now(), Lock::InterruptBehavior::kThrow);
- session.prepareTransaction(opCtx());
- ASSERT_THROWS_CODE(session.commitPreparedTransaction(opCtx(), Timestamp()),
- AssertionException,
- ErrorCodes::InvalidOptions);
-}
-
-// This test makes sure the abort machinery works even when no operations are done on the
-// transaction.
-TEST_F(SessionTest, EmptyTransactionAbort) {
- const auto sessionId = makeLogicalSessionIdForTest();
- Session session(sessionId);
- session.refreshFromStorageIfNeeded(opCtx());
-
- const TxnNumber txnNum = 26;
- opCtx()->setLogicalSessionId(sessionId);
- opCtx()->setTxnNumber(txnNum);
- session.beginOrContinueTxn(opCtx(), txnNum, false, true, "admin", "abortTransaction");
- session.unstashTransactionResources(opCtx(), "abortTransaction");
- // The transaction machinery cannot store an empty locker.
- { Lock::GlobalLock lk(opCtx(), MODE_IX, Date_t::now(), Lock::InterruptBehavior::kThrow); }
- session.stashTransactionResources(opCtx());
- session.abortArbitraryTransaction();
- ASSERT_TRUE(session.transactionIsAborted());
-}
-
-TEST_F(SessionTest, ConcurrencyOfUnstashAndAbort) {
- const auto sessionId = makeLogicalSessionIdForTest();
- Session session(sessionId);
- session.refreshFromStorageIfNeeded(opCtx());
-
- const TxnNumber txnNum = 26;
- opCtx()->setLogicalSessionId(sessionId);
- opCtx()->setTxnNumber(txnNum);
- session.beginOrContinueTxn(opCtx(), txnNum, false, true, "testDB", "find");
-
- // The transaction may be aborted without checking out the session.
- session.abortArbitraryTransaction();
-
- // An unstash after an abort should uassert.
- ASSERT_THROWS_CODE(session.unstashTransactionResources(opCtx(), "find"),
- AssertionException,
- ErrorCodes::NoSuchTransaction);
-}
-
-TEST_F(SessionTest, ConcurrencyOfUnstashAndMigration) {
- const auto sessionId = makeLogicalSessionIdForTest();
- Session session(sessionId);
- session.refreshFromStorageIfNeeded(opCtx());
-
- const TxnNumber txnNum = 26;
- opCtx()->setLogicalSessionId(sessionId);
- opCtx()->setTxnNumber(txnNum);
- session.beginOrContinueTxn(opCtx(), txnNum, false, true, "testDB", "insert");
-
- session.unstashTransactionResources(opCtx(), "insert");
- // The transaction machinery cannot store an empty locker.
- { Lock::GlobalLock lk(opCtx(), MODE_IX, Date_t::now(), Lock::InterruptBehavior::kThrow); }
- auto operation = repl::OplogEntry::makeInsertOperation(kNss, kUUID, BSON("TestValue" << 0));
- session.addTransactionOperation(opCtx(), operation);
- session.stashTransactionResources(opCtx());
-
- // A migration may bump the active transaction number without checking out the session.
- const TxnNumber higherTxnNum = 27;
- bumpTxnNumberFromDifferentOpCtx(&session, higherTxnNum);
-
- // An unstash after a migration that bumps the active transaction number should uassert.
- ASSERT_THROWS_CODE(session.unstashTransactionResources(opCtx(), "insert"),
- AssertionException,
- ErrorCodes::ConflictingOperationInProgress);
-}
-
-TEST_F(SessionTest, ConcurrencyOfStashAndAbort) {
- const auto sessionId = makeLogicalSessionIdForTest();
- Session session(sessionId);
- session.refreshFromStorageIfNeeded(opCtx());
-
- const TxnNumber txnNum = 26;
- opCtx()->setLogicalSessionId(sessionId);
- opCtx()->setTxnNumber(txnNum);
- session.beginOrContinueTxn(opCtx(), txnNum, false, true, "testDB", "find");
-
- session.unstashTransactionResources(opCtx(), "find");
-
- // The transaction may be aborted without checking out the session.
- session.abortArbitraryTransaction();
-
- // A stash after an abort should be a noop.
- session.stashTransactionResources(opCtx());
-}
-
-TEST_F(SessionTest, ConcurrencyOfStashAndMigration) {
- const auto sessionId = makeLogicalSessionIdForTest();
- Session session(sessionId);
- session.refreshFromStorageIfNeeded(opCtx());
-
- const TxnNumber txnNum = 26;
- opCtx()->setLogicalSessionId(sessionId);
- opCtx()->setTxnNumber(txnNum);
- session.beginOrContinueTxn(opCtx(), txnNum, false, true, "testDB", "insert");
-
- session.unstashTransactionResources(opCtx(), "insert");
- auto operation = repl::OplogEntry::makeInsertOperation(kNss, kUUID, BSON("TestValue" << 0));
- session.addTransactionOperation(opCtx(), operation);
-
- // A migration may bump the active transaction number without checking out the session.
- const TxnNumber higherTxnNum = 27;
- bumpTxnNumberFromDifferentOpCtx(&session, higherTxnNum);
-
- // A stash after a migration that bumps the active transaction number should uassert.
- ASSERT_THROWS_CODE(session.stashTransactionResources(opCtx()),
- AssertionException,
- ErrorCodes::ConflictingOperationInProgress);
-}
-
-TEST_F(SessionTest, ConcurrencyOfAddTransactionOperationAndAbort) {
- const auto sessionId = makeLogicalSessionIdForTest();
- Session session(sessionId);
- session.refreshFromStorageIfNeeded(opCtx());
-
- const TxnNumber txnNum = 26;
- opCtx()->setLogicalSessionId(sessionId);
- opCtx()->setTxnNumber(txnNum);
- session.beginOrContinueTxn(opCtx(), txnNum, false, true, "testDB", "insert");
-
- session.unstashTransactionResources(opCtx(), "insert");
-
- // The transaction may be aborted without checking out the session.
- session.abortArbitraryTransaction();
-
- // An addTransactionOperation() after an abort should uassert.
- auto operation = repl::OplogEntry::makeInsertOperation(kNss, kUUID, BSON("TestValue" << 0));
- ASSERT_THROWS_CODE(session.addTransactionOperation(opCtx(), operation),
- AssertionException,
- ErrorCodes::NoSuchTransaction);
-}
-
-TEST_F(SessionTest, ConcurrencyOfAddTransactionOperationAndMigration) {
- const auto sessionId = makeLogicalSessionIdForTest();
- Session session(sessionId);
- session.refreshFromStorageIfNeeded(opCtx());
-
- const TxnNumber txnNum = 26;
- opCtx()->setLogicalSessionId(sessionId);
- opCtx()->setTxnNumber(txnNum);
- session.beginOrContinueTxn(opCtx(), txnNum, false, true, "testDB", "find");
-
- session.unstashTransactionResources(opCtx(), "find");
- auto operation = repl::OplogEntry::makeInsertOperation(kNss, kUUID, BSON("TestValue" << 0));
- session.addTransactionOperation(opCtx(), operation);
-
- // A migration may bump the active transaction number without checking out the session.
- const TxnNumber higherTxnNum = 27;
- bumpTxnNumberFromDifferentOpCtx(&session, higherTxnNum);
-
- // An addTransactionOperation() after a migration that bumps the active transaction number
- // should uassert.
- ASSERT_THROWS_CODE(session.addTransactionOperation(opCtx(), operation),
- AssertionException,
- ErrorCodes::ConflictingOperationInProgress);
-}
-
-TEST_F(SessionTest, ConcurrencyOfEndTransactionAndRetrieveOperationsAndAbort) {
- const auto sessionId = makeLogicalSessionIdForTest();
- Session session(sessionId);
- session.refreshFromStorageIfNeeded(opCtx());
-
- const TxnNumber txnNum = 26;
- opCtx()->setLogicalSessionId(sessionId);
- opCtx()->setTxnNumber(txnNum);
- session.beginOrContinueTxn(opCtx(), txnNum, false, true, "testDB", "insert");
-
- session.unstashTransactionResources(opCtx(), "insert");
-
- // The transaction may be aborted without checking out the session.
- session.abortArbitraryTransaction();
-
- // An endTransactionAndRetrieveOperations() after an abort should uassert.
- ASSERT_THROWS_CODE(session.endTransactionAndRetrieveOperations(opCtx()),
- AssertionException,
- ErrorCodes::NoSuchTransaction);
-}
-
-TEST_F(SessionTest, ConcurrencyOfEndTransactionAndRetrieveOperationsAndMigration) {
- const auto sessionId = makeLogicalSessionIdForTest();
- Session session(sessionId);
- session.refreshFromStorageIfNeeded(opCtx());
-
- const TxnNumber txnNum = 26;
- opCtx()->setLogicalSessionId(sessionId);
- opCtx()->setTxnNumber(txnNum);
- session.beginOrContinueTxn(opCtx(), txnNum, false, true, "testDB", "insert");
-
- session.unstashTransactionResources(opCtx(), "insert");
- auto operation = repl::OplogEntry::makeInsertOperation(kNss, kUUID, BSON("TestValue" << 0));
- session.addTransactionOperation(opCtx(), operation);
-
- // A migration may bump the active transaction number without checking out the session.
- const TxnNumber higherTxnNum = 27;
- bumpTxnNumberFromDifferentOpCtx(&session, higherTxnNum);
-
- // An endTransactionAndRetrieveOperations() after a migration that bumps the active transaction
- // number should uassert.
- ASSERT_THROWS_CODE(session.endTransactionAndRetrieveOperations(opCtx()),
- AssertionException,
- ErrorCodes::ConflictingOperationInProgress);
-}
-
-TEST_F(SessionTest, ConcurrencyOfCommitTransactionAndAbort) {
- const auto sessionId = makeLogicalSessionIdForTest();
- Session session(sessionId);
- session.refreshFromStorageIfNeeded(opCtx());
-
- const TxnNumber txnNum = 26;
- opCtx()->setLogicalSessionId(sessionId);
- opCtx()->setTxnNumber(txnNum);
- session.beginOrContinueTxn(opCtx(), txnNum, false, true, "admin", "commitTransaction");
-
- session.unstashTransactionResources(opCtx(), "commitTransaction");
-
- // The transaction may be aborted without checking out the session.
- session.abortArbitraryTransaction();
-
- // An commitTransaction() after an abort should uassert.
- ASSERT_THROWS_CODE(session.commitUnpreparedTransaction(opCtx()),
- AssertionException,
- ErrorCodes::NoSuchTransaction);
-}
-
-TEST_F(SessionTest, ConcurrencyOfActiveAbortAndArbitraryAbort) {
- const auto sessionId = makeLogicalSessionIdForTest();
- Session session(sessionId);
- session.refreshFromStorageIfNeeded(opCtx());
-
- const TxnNumber txnNum = 26;
- opCtx()->setLogicalSessionId(sessionId);
- opCtx()->setTxnNumber(txnNum);
- session.beginOrContinueTxn(opCtx(), txnNum, false, true, "testDB", "insert");
-
- session.unstashTransactionResources(opCtx(), "insert");
- ASSERT(session.inMultiDocumentTransaction());
-
- // The transaction may be aborted without checking out the session.
- session.abortArbitraryTransaction();
-
- // The operation throws for some reason and aborts implicitly.
- // Abort active transaction after it's been aborted by KillSession is a no-op.
- session.abortActiveTransaction(opCtx());
- ASSERT(session.transactionIsAborted());
- ASSERT(opCtx()->getWriteUnitOfWork() == nullptr);
-}
-
-TEST_F(SessionTest, ConcurrencyOfActiveAbortAndMigration) {
- const auto sessionId = makeLogicalSessionIdForTest();
- Session session(sessionId);
- session.refreshFromStorageIfNeeded(opCtx());
-
- const TxnNumber txnNum = 26;
- opCtx()->setLogicalSessionId(sessionId);
- opCtx()->setTxnNumber(txnNum);
- session.beginOrContinueTxn(opCtx(), txnNum, false, true, "testDB", "insert");
-
- session.unstashTransactionResources(opCtx(), "insert");
- auto operation = repl::OplogEntry::makeInsertOperation(kNss, kUUID, BSON("TestValue" << 0));
- session.addTransactionOperation(opCtx(), operation);
- ASSERT(session.inMultiDocumentTransaction());
-
- // A migration may bump the active transaction number without checking out the session.
- const TxnNumber higherTxnNum = 27;
- bumpTxnNumberFromDifferentOpCtx(&session, higherTxnNum);
-
- // The operation throws for some reason and aborts implicitly.
- // Abort active transaction after it's been aborted by migration is a no-op.
- session.abortActiveTransaction(opCtx());
- // The session's state is None after migration, but we should have cleared
- // the states of opCtx.
- ASSERT(opCtx()->getWriteUnitOfWork() == nullptr);
-}
-
-TEST_F(SessionTest, ConcurrencyOfPrepareTransactionAndAbort) {
- const auto sessionId = makeLogicalSessionIdForTest();
- Session session(sessionId);
- session.refreshFromStorageIfNeeded(opCtx());
-
- const TxnNumber txnNum = 26;
- opCtx()->setLogicalSessionId(sessionId);
- opCtx()->setTxnNumber(txnNum);
- session.beginOrContinueTxn(opCtx(), txnNum, false, true, "admin", "prepareTransaction");
-
- session.unstashTransactionResources(opCtx(), "prepareTransaction");
-
- // The transaction may be aborted without checking out the session.
- session.abortArbitraryTransaction();
- ASSERT(session.transactionIsAborted());
-
- // A prepareTransaction() after an abort should uassert.
- ASSERT_THROWS_CODE(
- session.prepareTransaction(opCtx()), AssertionException, ErrorCodes::NoSuchTransaction);
- ASSERT_FALSE(_opObserver->transactionPrepared);
- ASSERT(session.transactionIsAborted());
-}
-
-TEST_F(SessionTest, ConcurrencyOfPrepareTransactionAndCommit) {
- const auto sessionId = makeLogicalSessionIdForTest();
- Session session(sessionId);
- session.refreshFromStorageIfNeeded(opCtx());
-
- const TxnNumber txnNum = 26;
- opCtx()->setLogicalSessionId(sessionId);
- opCtx()->setTxnNumber(txnNum);
- session.beginOrContinueTxn(opCtx(), txnNum, false, true, "admin", "prepareTransaction");
-
- session.unstashTransactionResources(opCtx(), "prepareTransaction");
-
- session.commitUnpreparedTransaction(opCtx());
-
- // A prepareTransaction() after a commit should uassert.
- ASSERT_THROWS_CODE(
- session.prepareTransaction(opCtx()), AssertionException, ErrorCodes::TransactionCommitted);
- ASSERT_FALSE(_opObserver->transactionPrepared);
- ASSERT(session.transactionIsCommitted());
-}
-
-TEST_F(SessionTest, KillSessionsDuringPrepareDoesNotAbortTransaction) {
- const auto sessionId = makeLogicalSessionIdForTest();
- Session session(sessionId);
- session.refreshFromStorageIfNeeded(opCtx());
-
- const TxnNumber txnNum = 26;
- opCtx()->setLogicalSessionId(sessionId);
- opCtx()->setTxnNumber(txnNum);
- session.beginOrContinueTxn(opCtx(), txnNum, false, true, "admin", "prepareTransaction");
-
- session.unstashTransactionResources(opCtx(), "prepareTransaction");
-
- auto ruPrepareTimestamp = Timestamp();
- auto originalFn = _opObserver->onTransactionPrepareFn;
- _opObserver->onTransactionPrepareFn = [&]() {
- originalFn();
-
- ruPrepareTimestamp = opCtx()->recoveryUnit()->getPrepareTimestamp();
- ASSERT_FALSE(ruPrepareTimestamp.isNull());
- // The transaction may be aborted without checking out the session.
- session.abortArbitraryTransaction();
- ASSERT_FALSE(session.transactionIsAborted());
- };
-
- // Check that prepareTimestamp gets set.
- auto prepareTimestamp = session.prepareTransaction(opCtx());
- ASSERT_EQ(ruPrepareTimestamp, prepareTimestamp);
- ASSERT(_opObserver->transactionPrepared);
- ASSERT_FALSE(session.transactionIsAborted());
-}
-
-DEATH_TEST_F(SessionTest, AbortDuringPrepareIsFatal, "Fatal assertion 50906") {
- const auto sessionId = makeLogicalSessionIdForTest();
- Session session(sessionId);
- session.refreshFromStorageIfNeeded(opCtx());
-
- const TxnNumber txnNum = 26;
- opCtx()->setLogicalSessionId(sessionId);
- opCtx()->setTxnNumber(txnNum);
- session.beginOrContinueTxn(opCtx(), txnNum, false, true, "admin", "prepareTransaction");
-
- session.unstashTransactionResources(opCtx(), "prepareTransaction");
-
- auto originalFn = _opObserver->onTransactionPrepareFn;
- _opObserver->onTransactionPrepareFn = [&]() {
- originalFn();
-
- // The transaction may be aborted without checking out the session.
- session.abortActiveTransaction(opCtx());
- ASSERT(session.transactionIsAborted());
- };
-
- session.prepareTransaction(opCtx());
-}
-
-TEST_F(SessionTest, ThrowDuringOnTransactionPrepareAbortsTransaction) {
- const auto sessionId = makeLogicalSessionIdForTest();
- Session session(sessionId);
- session.refreshFromStorageIfNeeded(opCtx());
-
- const TxnNumber txnNum = 26;
- opCtx()->setLogicalSessionId(sessionId);
- opCtx()->setTxnNumber(txnNum);
- session.beginOrContinueTxn(opCtx(), txnNum, false, true, "admin", "prepareTransaction");
-
- session.unstashTransactionResources(opCtx(), "prepareTransaction");
-
- _opObserver->onTransactionPrepareThrowsException = true;
-
- ASSERT_THROWS_CODE(
- session.prepareTransaction(opCtx()), AssertionException, ErrorCodes::OperationFailed);
- ASSERT_FALSE(_opObserver->transactionPrepared);
- ASSERT(session.transactionIsAborted());
-}
-
-TEST_F(SessionTest, KillSessionsDuringPreparedCommitDoesNotAbortTransaction) {
- const auto sessionId = makeLogicalSessionIdForTest();
- Session session(sessionId);
- session.refreshFromStorageIfNeeded(opCtx());
-
- const auto commitTimestamp = Timestamp(1, 1);
- const TxnNumber txnNum = 26;
- opCtx()->setLogicalSessionId(sessionId);
- opCtx()->setTxnNumber(txnNum);
- session.beginOrContinueTxn(opCtx(), txnNum, false, true, "admin", "commitTransaction");
-
- session.unstashTransactionResources(opCtx(), "commitTransaction");
-
- auto originalFn = _opObserver->onTransactionCommitFn;
- _opObserver->onTransactionCommitFn = [&](bool wasPrepared) {
- originalFn(wasPrepared);
- ASSERT(wasPrepared);
-
- // The transaction may be aborted without checking out the session.
- session.abortArbitraryTransaction();
- ASSERT_FALSE(session.transactionIsAborted());
- };
-
- session.prepareTransaction(opCtx());
- session.commitPreparedTransaction(opCtx(), commitTimestamp);
- ASSERT(_opObserver->transactionCommitted);
- ASSERT_FALSE(session.transactionIsAborted());
- ASSERT(session.transactionIsCommitted());
-}
-
-// This tests documents behavior, though it is not necessarily the behavior we want.
-TEST_F(SessionTest, AbortDuringPreparedCommitDoesNotAbortTransaction) {
- const auto sessionId = makeLogicalSessionIdForTest();
- Session session(sessionId);
- session.refreshFromStorageIfNeeded(opCtx());
-
- const auto commitTimestamp = Timestamp(1, 1);
- const TxnNumber txnNum = 26;
- opCtx()->setLogicalSessionId(sessionId);
- opCtx()->setTxnNumber(txnNum);
- session.beginOrContinueTxn(opCtx(), txnNum, false, true, "admin", "commitTransaction");
-
- session.unstashTransactionResources(opCtx(), "commitTransaction");
-
- auto originalFn = _opObserver->onTransactionCommitFn;
- _opObserver->onTransactionCommitFn = [&](bool wasPrepared) {
- originalFn(wasPrepared);
- ASSERT(wasPrepared);
-
- // The transaction may be aborted without checking out the session.
- auto func = [&](OperationContext* opCtx) { session.abortActiveTransaction(opCtx); };
- runFunctionFromDifferentOpCtx(func);
- ASSERT_FALSE(session.transactionIsAborted());
- };
-
- session.prepareTransaction(opCtx());
- session.commitPreparedTransaction(opCtx(), commitTimestamp);
- ASSERT(_opObserver->transactionCommitted);
- ASSERT_FALSE(session.transactionIsAborted());
- ASSERT(session.transactionIsCommitted());
-}
-
-// This tests documents behavior, though it is not necessarily the behavior we want.
-TEST_F(SessionTest, ThrowDuringPreparedOnTransactionCommitDoesNothing) {
- const auto sessionId = makeLogicalSessionIdForTest();
- Session session(sessionId);
- session.refreshFromStorageIfNeeded(opCtx());
-
- const auto commitTimestamp = Timestamp(1, 1);
- const TxnNumber txnNum = 26;
- opCtx()->setLogicalSessionId(sessionId);
- opCtx()->setTxnNumber(txnNum);
- session.beginOrContinueTxn(opCtx(), txnNum, false, true, "admin", "commitTransaction");
-
- session.unstashTransactionResources(opCtx(), "commitTransaction");
-
- _opObserver->onTransactionCommitThrowsException = true;
-
- session.prepareTransaction(opCtx());
- ASSERT_THROWS_CODE(session.commitPreparedTransaction(opCtx(), commitTimestamp),
- AssertionException,
- ErrorCodes::OperationFailed);
- ASSERT_FALSE(_opObserver->transactionCommitted);
- ASSERT_FALSE(session.transactionIsAborted());
- ASSERT_FALSE(session.transactionIsCommitted());
-}
-
-TEST_F(SessionTest, KillSessionsDuringUnpreparedCommitDoesNotAbortTransaction) {
- const auto sessionId = makeLogicalSessionIdForTest();
- Session session(sessionId);
- session.refreshFromStorageIfNeeded(opCtx());
-
- const TxnNumber txnNum = 26;
- opCtx()->setLogicalSessionId(sessionId);
- opCtx()->setTxnNumber(txnNum);
- session.beginOrContinueTxn(opCtx(), txnNum, false, true, "admin", "commitTransaction");
-
- session.unstashTransactionResources(opCtx(), "commitTransaction");
-
- auto originalFn = _opObserver->onTransactionCommitFn;
- _opObserver->onTransactionCommitFn = [&](bool wasPrepared) {
- originalFn(wasPrepared);
- ASSERT_FALSE(wasPrepared);
-
- // The transaction may be aborted without checking out the session.
- session.abortArbitraryTransaction();
- ASSERT_FALSE(session.transactionIsAborted());
- };
-
- session.commitUnpreparedTransaction(opCtx());
- ASSERT(_opObserver->transactionCommitted);
- ASSERT_FALSE(session.transactionIsAborted());
- ASSERT(session.transactionIsCommitted());
-}
-
-TEST_F(SessionTest, AbortDuringUnpreparedCommitDoesNotAbortTransaction) {
- const auto sessionId = makeLogicalSessionIdForTest();
- Session session(sessionId);
- session.refreshFromStorageIfNeeded(opCtx());
-
- const TxnNumber txnNum = 26;
- opCtx()->setLogicalSessionId(sessionId);
- opCtx()->setTxnNumber(txnNum);
- session.beginOrContinueTxn(opCtx(), txnNum, false, true, "admin", "commitTransaction");
-
- session.unstashTransactionResources(opCtx(), "commitTransaction");
-
- auto originalFn = _opObserver->onTransactionCommitFn;
- _opObserver->onTransactionCommitFn = [&](bool wasPrepared) {
- originalFn(wasPrepared);
- ASSERT_FALSE(wasPrepared);
-
- // The transaction may be aborted without checking out the session.
- auto func = [&](OperationContext* opCtx) { session.abortActiveTransaction(opCtx); };
- runFunctionFromDifferentOpCtx(func);
- ASSERT_FALSE(session.transactionIsAborted());
- };
-
- session.commitUnpreparedTransaction(opCtx());
- ASSERT(_opObserver->transactionCommitted);
- ASSERT_FALSE(session.transactionIsAborted());
- ASSERT(session.transactionIsCommitted());
-}
-
-// This tests documents behavior, though it is not necessarily the behavior we want.
-TEST_F(SessionTest, ThrowDuringUnpreparedOnTransactionCommitDoesNothing) {
- const auto sessionId = makeLogicalSessionIdForTest();
- Session session(sessionId);
- session.refreshFromStorageIfNeeded(opCtx());
-
- const TxnNumber txnNum = 26;
- opCtx()->setLogicalSessionId(sessionId);
- opCtx()->setTxnNumber(txnNum);
- session.beginOrContinueTxn(opCtx(), txnNum, false, true, "admin", "commitTransaction");
-
- session.unstashTransactionResources(opCtx(), "commitTransaction");
-
- _opObserver->onTransactionCommitThrowsException = true;
-
- ASSERT_THROWS_CODE(session.commitUnpreparedTransaction(opCtx()),
- AssertionException,
- ErrorCodes::OperationFailed);
- ASSERT_FALSE(_opObserver->transactionCommitted);
- ASSERT_FALSE(session.transactionIsAborted());
- ASSERT_FALSE(session.transactionIsCommitted());
-}
-
-TEST_F(SessionTest, ConcurrencyOfCommitTransactionAndMigration) {
- const auto sessionId = makeLogicalSessionIdForTest();
- Session session(sessionId);
- session.refreshFromStorageIfNeeded(opCtx());
-
- const TxnNumber txnNum = 26;
- opCtx()->setLogicalSessionId(sessionId);
- opCtx()->setTxnNumber(txnNum);
- session.beginOrContinueTxn(opCtx(), txnNum, false, true, "testDB", "insert");
-
- session.unstashTransactionResources(opCtx(), "insert");
- auto operation = repl::OplogEntry::makeInsertOperation(kNss, kUUID, BSON("TestValue" << 0));
- session.addTransactionOperation(opCtx(), operation);
-
- // A migration may bump the active transaction number without checking out the session.
- const TxnNumber higherTxnNum = 27;
- bumpTxnNumberFromDifferentOpCtx(&session, higherTxnNum);
-
- // An commitTransaction() after a migration that bumps the active transaction number should
- // uassert.
- ASSERT_THROWS_CODE(session.commitUnpreparedTransaction(opCtx()),
- AssertionException,
- ErrorCodes::ConflictingOperationInProgress);
-}
-
-TEST_F(SessionTest, ConcurrencyOfPrepareTransactionAndMigration) {
- const auto sessionId = makeLogicalSessionIdForTest();
- Session session(sessionId);
- session.refreshFromStorageIfNeeded(opCtx());
-
- const TxnNumber txnNum = 26;
- opCtx()->setLogicalSessionId(sessionId);
- opCtx()->setTxnNumber(txnNum);
- session.beginOrContinueTxn(opCtx(), txnNum, false, true, "testDB", "insert");
-
- session.unstashTransactionResources(opCtx(), "insert");
- auto operation = repl::OplogEntry::makeInsertOperation(kNss, kUUID, BSON("TestValue" << 0));
- session.addTransactionOperation(opCtx(), operation);
-
- // A migration may bump the active transaction number without checking out the session.
- const TxnNumber higherTxnNum = 27;
- bumpTxnNumberFromDifferentOpCtx(&session, higherTxnNum);
-
- // A prepareTransaction() after a migration that bumps the active transaction number should
- // uassert.
- ASSERT_THROWS_CODE(session.prepareTransaction(opCtx()),
- AssertionException,
- ErrorCodes::ConflictingOperationInProgress);
- ASSERT_FALSE(_opObserver->transactionPrepared);
-}
-
-TEST_F(SessionTest, ContinuingATransactionWithNoResourcesAborts) {
- const auto sessionId = makeLogicalSessionIdForTest();
- Session session(sessionId);
- session.refreshFromStorageIfNeeded(opCtx());
-
- const TxnNumber txnNum = 26;
- opCtx()->setLogicalSessionId(sessionId);
- opCtx()->setTxnNumber(txnNum);
- session.beginOrContinueTxn(opCtx(), txnNum, false, true, "testDB", "insert");
-
- ASSERT_THROWS_CODE(
- session.beginOrContinueTxn(opCtx(), txnNum, false, boost::none, "testDB", "insert"),
- AssertionException,
- ErrorCodes::NoSuchTransaction);
-}
-
-TEST_F(SessionTest, KillSessionsDoesNotAbortPreparedTransactions) {
- const auto sessionId = makeLogicalSessionIdForTest();
- Session session(sessionId);
- session.refreshFromStorageIfNeeded(opCtx());
-
- const TxnNumber txnNum = 26;
- opCtx()->setLogicalSessionId(sessionId);
- opCtx()->setTxnNumber(txnNum);
- session.beginOrContinueTxn(opCtx(), txnNum, false, true, "testDB", "insert");
-
- session.unstashTransactionResources(opCtx(), "insert");
-
- auto ruPrepareTimestamp = Timestamp();
- auto originalFn = _opObserver->onTransactionPrepareFn;
- _opObserver->onTransactionPrepareFn = [&]() {
- originalFn();
-
- ruPrepareTimestamp = opCtx()->recoveryUnit()->getPrepareTimestamp();
- ASSERT_FALSE(ruPrepareTimestamp.isNull());
- };
-
- // Check that prepareTimestamp gets set.
- auto prepareTimestamp = session.prepareTransaction(opCtx());
- ASSERT_EQ(ruPrepareTimestamp, prepareTimestamp);
- session.stashTransactionResources(opCtx());
-
- session.abortArbitraryTransaction();
- ASSERT_FALSE(session.transactionIsAborted());
- ASSERT(_opObserver->transactionPrepared);
-}
-
-TEST_F(SessionTest, CannotStartNewTransactionWhilePreparedTransactionInProgress) {
- const auto sessionId = makeLogicalSessionIdForTest();
- Session session(sessionId);
- session.refreshFromStorageIfNeeded(opCtx());
-
- const TxnNumber txnNum = 26;
- opCtx()->setLogicalSessionId(sessionId);
- opCtx()->setTxnNumber(txnNum);
- session.beginOrContinueTxn(opCtx(), txnNum, false, true, "testDB", "insert");
-
- session.unstashTransactionResources(opCtx(), "insert");
-
- auto ruPrepareTimestamp = Timestamp();
- auto originalFn = _opObserver->onTransactionPrepareFn;
- _opObserver->onTransactionPrepareFn = [&]() {
- originalFn();
-
- ruPrepareTimestamp = opCtx()->recoveryUnit()->getPrepareTimestamp();
- ASSERT_FALSE(ruPrepareTimestamp.isNull());
- };
-
- // Check that prepareTimestamp gets set.
- auto prepareTimestamp = session.prepareTransaction(opCtx());
- ASSERT_EQ(ruPrepareTimestamp, prepareTimestamp);
-
- session.stashTransactionResources(opCtx());
-
- // Try to start a new transaction while there is already a prepared transaction on the
- // session. This should fail with a PreparedTransactionInProgress error.
- const TxnNumber txnNum2 = 27;
- ASSERT_THROWS_CODE(
- session.beginOrContinueTxn(opCtx(), txnNum2, false, true, "testDB", "insert"),
- AssertionException,
- ErrorCodes::PreparedTransactionInProgress);
-
- ASSERT_FALSE(session.transactionIsAborted());
- ASSERT(_opObserver->transactionPrepared);
-}
-
-// Tests that a transaction aborts if it becomes too large before trying to commit it.
-TEST_F(SessionTest, TransactionTooLargeWhileBuilding) {
- const auto sessionId = makeLogicalSessionIdForTest();
- Session session(sessionId);
- session.refreshFromStorageIfNeeded(opCtx());
-
- const TxnNumber txnNum = 28;
- opCtx()->setLogicalSessionId(sessionId);
- opCtx()->setTxnNumber(txnNum);
- session.beginOrContinueTxn(opCtx(), txnNum, false, true, "testDB", "insert");
-
- session.unstashTransactionResources(opCtx(), "insert");
-
- // Two 6MB operations should succeed; three 6MB operations should fail.
- constexpr size_t kBigDataSize = 6 * 1024 * 1024;
- std::unique_ptr<uint8_t[]> bigData(new uint8_t[kBigDataSize]());
- auto operation = repl::OplogEntry::makeInsertOperation(
- kNss,
- kUUID,
- BSON("_id" << 0 << "data" << BSONBinData(bigData.get(), kBigDataSize, BinDataGeneral)));
- session.addTransactionOperation(opCtx(), operation);
- session.addTransactionOperation(opCtx(), operation);
- ASSERT_THROWS_CODE(session.addTransactionOperation(opCtx(), operation),
- AssertionException,
- ErrorCodes::TransactionTooLarge);
-}
-
-TEST_F(SessionTest, IncrementTotalStartedUponStartTransaction) {
- const auto sessionId = makeLogicalSessionIdForTest();
- Session session(sessionId);
- session.refreshFromStorageIfNeeded(opCtx());
-
- unsigned long long beforeTransactionStart =
- ServerTransactionsMetrics::get(opCtx())->getTotalStarted();
-
- const TxnNumber txnNum = 1;
- session.beginOrContinueTxn(opCtx(), txnNum, false, true, "testDB", "insert");
-
- // Tests that the total transactions started counter is incremented by 1 when a new transaction
- // is started.
- ASSERT_EQ(ServerTransactionsMetrics::get(opCtx())->getTotalStarted(),
- beforeTransactionStart + 1U);
-}
-
-TEST_F(SessionTest, IncrementTotalCommittedOnCommit) {
- const auto sessionId = makeLogicalSessionIdForTest();
- Session session(sessionId);
- session.refreshFromStorageIfNeeded(opCtx());
-
- const TxnNumber txnNum = 1;
- opCtx()->setLogicalSessionId(sessionId);
- opCtx()->setTxnNumber(txnNum);
- session.beginOrContinueTxn(opCtx(), txnNum, false, true, "admin", "commitTransaction");
- session.unstashTransactionResources(opCtx(), "commitTransaction");
-
- unsigned long long beforeCommitCount =
- ServerTransactionsMetrics::get(opCtx())->getTotalCommitted();
-
- session.commitUnpreparedTransaction(opCtx());
-
- // Assert that the committed counter is incremented by 1.
- ASSERT_EQ(ServerTransactionsMetrics::get(opCtx())->getTotalCommitted(), beforeCommitCount + 1U);
-}
-
-TEST_F(SessionTest, IncrementTotalAbortedUponAbort) {
- const auto sessionId = makeLogicalSessionIdForTest();
- Session session(sessionId);
- session.refreshFromStorageIfNeeded(opCtx());
-
- const TxnNumber txnNum = 1;
- opCtx()->setLogicalSessionId(sessionId);
- opCtx()->setTxnNumber(txnNum);
- session.beginOrContinueTxn(opCtx(), txnNum, false, true, "testDB", "insert");
- session.unstashTransactionResources(opCtx(), "insert");
-
- unsigned long long beforeAbortCount =
- ServerTransactionsMetrics::get(opCtx())->getTotalAborted();
-
- session.abortArbitraryTransaction();
-
- // Assert that the aborted counter is incremented by 1.
- ASSERT_EQ(ServerTransactionsMetrics::get(opCtx())->getTotalAborted(), beforeAbortCount + 1U);
-}
-
-TEST_F(SessionTest, TrackTotalOpenTransactionsWithAbort) {
- const auto sessionId = makeLogicalSessionIdForTest();
- Session session(sessionId);
- session.refreshFromStorageIfNeeded(opCtx());
-
- unsigned long long beforeTransactionStart =
- ServerTransactionsMetrics::get(opCtx())->getCurrentOpen();
-
- const TxnNumber txnNum = 1;
- opCtx()->setLogicalSessionId(sessionId);
- opCtx()->setTxnNumber(txnNum);
-
- // Tests that starting a transaction increments the open transactions counter by 1.
- session.beginOrContinueTxn(opCtx(), txnNum, false, true, "testDB", "insert");
- session.unstashTransactionResources(opCtx(), "insert");
- ASSERT_EQ(ServerTransactionsMetrics::get(opCtx())->getCurrentOpen(),
- beforeTransactionStart + 1U);
-
- // Tests that stashing the transaction resources does not affect the open transactions counter.
- { Lock::GlobalLock lk(opCtx(), MODE_IX, Date_t::now(), Lock::InterruptBehavior::kThrow); }
- session.stashTransactionResources(opCtx());
- ASSERT_EQ(ServerTransactionsMetrics::get(opCtx())->getCurrentOpen(),
- beforeTransactionStart + 1U);
-
- // Tests that aborting a transaction decrements the open transactions counter by 1.
- session.abortArbitraryTransaction();
- ASSERT_EQ(ServerTransactionsMetrics::get(opCtx())->getCurrentOpen(), beforeTransactionStart);
-}
-
-TEST_F(SessionTest, TrackTotalOpenTransactionsWithCommit) {
- const auto sessionId = makeLogicalSessionIdForTest();
- Session session(sessionId);
- session.refreshFromStorageIfNeeded(opCtx());
-
- unsigned long long beforeTransactionStart =
- ServerTransactionsMetrics::get(opCtx())->getCurrentOpen();
-
- const TxnNumber txnNum = 1;
- opCtx()->setLogicalSessionId(sessionId);
- opCtx()->setTxnNumber(txnNum);
-
- // Tests that starting a transaction increments the open transactions counter by 1.
- session.beginOrContinueTxn(opCtx(), txnNum, false, true, "testDB", "insert");
- session.unstashTransactionResources(opCtx(), "insert");
- ASSERT_EQ(ServerTransactionsMetrics::get(opCtx())->getCurrentOpen(),
- beforeTransactionStart + 1U);
-
- // Tests that stashing the transaction resources does not affect the open transactions counter.
- { Lock::GlobalLock lk(opCtx(), MODE_IX, Date_t::now(), Lock::InterruptBehavior::kThrow); }
- session.stashTransactionResources(opCtx());
- ASSERT_EQ(ServerTransactionsMetrics::get(opCtx())->getCurrentOpen(),
- beforeTransactionStart + 1U);
-
- session.unstashTransactionResources(opCtx(), "insert");
-
- // Tests that committing a transaction decrements the open transactions counter by 1.
- session.commitUnpreparedTransaction(opCtx());
- ASSERT_EQ(ServerTransactionsMetrics::get(opCtx())->getCurrentOpen(), beforeTransactionStart);
-}
-
-TEST_F(SessionTest, TrackTotalActiveAndInactiveTransactionsWithCommit) {
- const auto sessionId = makeLogicalSessionIdForTest();
- Session session(sessionId);
- session.refreshFromStorageIfNeeded(opCtx());
-
- const TxnNumber txnNum = 1;
- opCtx()->setLogicalSessionId(sessionId);
- opCtx()->setTxnNumber(txnNum);
-
- unsigned long long beforeActiveCounter =
- ServerTransactionsMetrics::get(opCtx())->getCurrentActive();
- unsigned long long beforeInactiveCounter =
- ServerTransactionsMetrics::get(opCtx())->getCurrentInactive();
-
- // Tests that the first unstash only increments the active counter only.
- session.beginOrContinueTxn(opCtx(), txnNum, false, true, "testDB", "insert");
- session.unstashTransactionResources(opCtx(), "insert");
- ASSERT_EQ(ServerTransactionsMetrics::get(opCtx())->getCurrentActive(),
- beforeActiveCounter + 1U);
- ASSERT_EQ(ServerTransactionsMetrics::get(opCtx())->getCurrentInactive(), beforeInactiveCounter);
-
- // Tests that stashing the transaction resources decrements active counter and increments
- // inactive counter.
- { Lock::GlobalLock lk(opCtx(), MODE_IX, Date_t::now(), Lock::InterruptBehavior::kThrow); }
- session.stashTransactionResources(opCtx());
- ASSERT_EQ(ServerTransactionsMetrics::get(opCtx())->getCurrentActive(), beforeActiveCounter);
- ASSERT_EQ(ServerTransactionsMetrics::get(opCtx())->getCurrentInactive(),
- beforeInactiveCounter + 1U);
-
- // Tests that the second unstash increments the active counter and decrements the inactive
- // counter.
- session.unstashTransactionResources(opCtx(), "insert");
- ASSERT_EQ(ServerTransactionsMetrics::get(opCtx())->getCurrentActive(),
- beforeActiveCounter + 1U);
- ASSERT_EQ(ServerTransactionsMetrics::get(opCtx())->getCurrentInactive(), beforeInactiveCounter);
-
- // Tests that committing a transaction decrements the active counter only.
- session.commitUnpreparedTransaction(opCtx());
- ASSERT_EQ(ServerTransactionsMetrics::get(opCtx())->getCurrentActive(), beforeActiveCounter);
- ASSERT_EQ(ServerTransactionsMetrics::get(opCtx())->getCurrentInactive(), beforeInactiveCounter);
-}
-
-TEST_F(SessionTest, TrackTotalActiveAndInactiveTransactionsWithStashedAbort) {
- const auto sessionId = makeLogicalSessionIdForTest();
- Session session(sessionId);
- session.refreshFromStorageIfNeeded(opCtx());
-
- const TxnNumber txnNum = 1;
- opCtx()->setLogicalSessionId(sessionId);
- opCtx()->setTxnNumber(txnNum);
-
- unsigned long long beforeActiveCounter =
- ServerTransactionsMetrics::get(opCtx())->getCurrentActive();
- unsigned long long beforeInactiveCounter =
- ServerTransactionsMetrics::get(opCtx())->getCurrentInactive();
-
- // Tests that the first unstash only increments the active counter only.
- session.beginOrContinueTxn(opCtx(), txnNum, false, true, "testDB", "insert");
- session.unstashTransactionResources(opCtx(), "insert");
- ASSERT_EQ(ServerTransactionsMetrics::get(opCtx())->getCurrentActive(),
- beforeActiveCounter + 1U);
- ASSERT_EQ(ServerTransactionsMetrics::get(opCtx())->getCurrentInactive(), beforeInactiveCounter);
-
- // Tests that stashing the transaction resources decrements active counter and increments
- // inactive counter.
- { Lock::GlobalLock lk(opCtx(), MODE_IX, Date_t::now(), Lock::InterruptBehavior::kThrow); }
- session.stashTransactionResources(opCtx());
- ASSERT_EQ(ServerTransactionsMetrics::get(opCtx())->getCurrentActive(), beforeActiveCounter);
- ASSERT_EQ(ServerTransactionsMetrics::get(opCtx())->getCurrentInactive(),
- beforeInactiveCounter + 1U);
-
- // Tests that aborting a stashed transaction decrements the inactive counter only.
- session.abortArbitraryTransaction();
- ASSERT_EQ(ServerTransactionsMetrics::get(opCtx())->getCurrentActive(), beforeActiveCounter);
- ASSERT_EQ(ServerTransactionsMetrics::get(opCtx())->getCurrentInactive(), beforeInactiveCounter);
-}
-
-TEST_F(SessionTest, TrackTotalActiveAndInactiveTransactionsWithUnstashedAbort) {
- const auto sessionId = makeLogicalSessionIdForTest();
- Session session(sessionId);
- session.refreshFromStorageIfNeeded(opCtx());
-
- const TxnNumber txnNum = 1;
- opCtx()->setLogicalSessionId(sessionId);
- opCtx()->setTxnNumber(txnNum);
-
- unsigned long long beforeActiveCounter =
- ServerTransactionsMetrics::get(opCtx())->getCurrentActive();
- unsigned long long beforeInactiveCounter =
- ServerTransactionsMetrics::get(opCtx())->getCurrentInactive();
-
- // Tests that the first unstash only increments the active counter only.
- session.beginOrContinueTxn(opCtx(), txnNum, false, true, "testDB", "insert");
- session.unstashTransactionResources(opCtx(), "insert");
- ASSERT_EQ(ServerTransactionsMetrics::get(opCtx())->getCurrentActive(),
- beforeActiveCounter + 1U);
- ASSERT_EQ(ServerTransactionsMetrics::get(opCtx())->getCurrentInactive(), beforeInactiveCounter);
-
- // Tests that aborting a stashed transaction decrements the active counter only.
- session.abortArbitraryTransaction();
- ASSERT_EQ(ServerTransactionsMetrics::get(opCtx())->getCurrentActive(), beforeActiveCounter);
- ASSERT_EQ(ServerTransactionsMetrics::get(opCtx())->getCurrentInactive(), beforeInactiveCounter);
-}
-
-/**
- * Test fixture for transactions metrics.
- */
-class TransactionsMetricsTest : public SessionTest {};
-
-TEST_F(TransactionsMetricsTest, SingleTransactionStatsStartTimeShouldBeSetUponTransactionStart) {
- const auto sessionId = makeLogicalSessionIdForTest();
- Session session(sessionId);
- session.refreshFromStorageIfNeeded(opCtx());
-
- const TxnNumber txnNum = 1;
-
- // Save the time before the transaction is created.
- unsigned long long timeBeforeTxn = curTimeMicros64();
- session.beginOrContinueTxn(opCtx(), txnNum, false, true, "testDB", "insert");
- unsigned long long timeAfterTxn = curTimeMicros64();
-
- // Start time should be greater than or equal to the time before the transaction was created.
- ASSERT_GTE(session.getSingleTransactionStats()->getStartTime(), timeBeforeTxn);
-
- // Start time should be less than or equal to the time after the transaction was started.
- ASSERT_LTE(session.getSingleTransactionStats()->getStartTime(), timeAfterTxn);
-}
-
-TEST_F(TransactionsMetricsTest, SingleTransactionStatsDurationShouldBeSetUponCommit) {
- const auto sessionId = makeLogicalSessionIdForTest();
- Session session(sessionId);
- session.refreshFromStorageIfNeeded(opCtx());
-
- const TxnNumber txnNum = 1;
- opCtx()->setLogicalSessionId(sessionId);
- opCtx()->setTxnNumber(txnNum);
-
- unsigned long long timeBeforeTxnStart = curTimeMicros64();
- session.beginOrContinueTxn(opCtx(), txnNum, false, true, "admin", "commitTransaction");
- unsigned long long timeAfterTxnStart = curTimeMicros64();
- session.unstashTransactionResources(opCtx(), "commitTransaction");
- // The transaction machinery cannot store an empty locker.
- Lock::GlobalLock lk(opCtx(), MODE_IX, Date_t::now(), Lock::InterruptBehavior::kThrow);
-
- // Sleep here to allow enough time to elapse.
- sleepmillis(10);
-
- unsigned long long timeBeforeTxnCommit = curTimeMicros64();
- session.commitUnpreparedTransaction(opCtx());
- unsigned long long timeAfterTxnCommit = curTimeMicros64();
-
- ASSERT_GTE(session.getSingleTransactionStats()->getDuration(curTimeMicros64()),
- timeBeforeTxnCommit - timeAfterTxnStart);
-
- ASSERT_LTE(session.getSingleTransactionStats()->getDuration(curTimeMicros64()),
- timeAfterTxnCommit - timeBeforeTxnStart);
-}
-
-TEST_F(TransactionsMetricsTest, SingleTransactionStatsDurationShouldBeSetUponAbort) {
- const auto sessionId = makeLogicalSessionIdForTest();
- Session session(sessionId);
- session.refreshFromStorageIfNeeded(opCtx());
-
- const TxnNumber txnNum = 1;
- opCtx()->setLogicalSessionId(sessionId);
- opCtx()->setTxnNumber(txnNum);
-
- unsigned long long timeBeforeTxnStart = curTimeMicros64();
- session.beginOrContinueTxn(opCtx(), txnNum, false, true, "testDB", "insert");
- unsigned long long timeAfterTxnStart = curTimeMicros64();
- session.unstashTransactionResources(opCtx(), "insert");
-
- // Sleep here to allow enough time to elapse.
- sleepmillis(10);
-
- unsigned long long timeBeforeTxnAbort = curTimeMicros64();
- session.abortArbitraryTransaction();
- unsigned long long timeAfterTxnAbort = curTimeMicros64();
-
- ASSERT_GTE(session.getSingleTransactionStats()->getDuration(curTimeMicros64()),
- timeBeforeTxnAbort - timeAfterTxnStart);
-
- ASSERT_LTE(session.getSingleTransactionStats()->getDuration(curTimeMicros64()),
- timeAfterTxnAbort - timeBeforeTxnStart);
-}
-
-TEST_F(TransactionsMetricsTest, SingleTransactionStatsDurationShouldKeepIncreasingUntilCommit) {
- const auto sessionId = makeLogicalSessionIdForTest();
- Session session(sessionId);
- session.refreshFromStorageIfNeeded(opCtx());
-
- const TxnNumber txnNum = 1;
- opCtx()->setLogicalSessionId(sessionId);
- opCtx()->setTxnNumber(txnNum);
-
- session.beginOrContinueTxn(opCtx(), txnNum, false, true, "admin", "commitTransaction");
- session.unstashTransactionResources(opCtx(), "commitTransaction");
- // The transaction machinery cannot store an empty locker.
- Lock::GlobalLock lk(opCtx(), MODE_IX, Date_t::now(), Lock::InterruptBehavior::kThrow);
-
- // Save the transaction's duration at this point.
- unsigned long long txnDurationAfterStart =
- session.getSingleTransactionStats()->getDuration(curTimeMicros64());
- sleepmillis(10);
-
- // The transaction's duration should have increased.
- ASSERT_GT(session.getSingleTransactionStats()->getDuration(curTimeMicros64()),
- txnDurationAfterStart);
- sleepmillis(10);
- session.commitUnpreparedTransaction(opCtx());
- unsigned long long txnDurationAfterCommit =
- session.getSingleTransactionStats()->getDuration(curTimeMicros64());
-
- // The transaction has committed, so the duration should have not increased.
- ASSERT_EQ(session.getSingleTransactionStats()->getDuration(curTimeMicros64()),
- txnDurationAfterCommit);
-
- ASSERT_GT(txnDurationAfterCommit, txnDurationAfterStart);
-}
-
-TEST_F(TransactionsMetricsTest, SingleTransactionStatsDurationShouldKeepIncreasingUntilAbort) {
- const auto sessionId = makeLogicalSessionIdForTest();
- Session session(sessionId);
- session.refreshFromStorageIfNeeded(opCtx());
-
- const TxnNumber txnNum = 1;
- opCtx()->setLogicalSessionId(sessionId);
- opCtx()->setTxnNumber(txnNum);
-
- session.beginOrContinueTxn(opCtx(), txnNum, false, true, "testDB", "insert");
- session.unstashTransactionResources(opCtx(), "insert");
- // The transaction machinery cannot store an empty locker.
- Lock::GlobalLock lk(opCtx(), MODE_IX, Date_t::now(), Lock::InterruptBehavior::kThrow);
-
- // Save the transaction's duration at this point.
- unsigned long long txnDurationAfterStart =
- session.getSingleTransactionStats()->getDuration(curTimeMicros64());
- sleepmillis(10);
-
- // The transaction's duration should have increased.
- ASSERT_GT(session.getSingleTransactionStats()->getDuration(curTimeMicros64()),
- txnDurationAfterStart);
- sleepmillis(10);
- session.abortArbitraryTransaction();
- unsigned long long txnDurationAfterAbort =
- session.getSingleTransactionStats()->getDuration(curTimeMicros64());
-
- // The transaction has aborted, so the duration should have not increased.
- ASSERT_EQ(session.getSingleTransactionStats()->getDuration(curTimeMicros64()),
- txnDurationAfterAbort);
-
- ASSERT_GT(txnDurationAfterAbort, txnDurationAfterStart);
-}
-
-TEST_F(TransactionsMetricsTest, TimeActiveMicrosShouldBeSetUponUnstashAndStash) {
- const auto sessionId = makeLogicalSessionIdForTest();
- Session session(sessionId);
- session.refreshFromStorageIfNeeded(opCtx());
-
- const TxnNumber txnNum = 1;
- opCtx()->setLogicalSessionId(sessionId);
- opCtx()->setTxnNumber(txnNum);
- session.beginOrContinueTxn(opCtx(), txnNum, false, true, "testDB", "insert");
-
- // Time active should be zero.
- ASSERT_EQ(session.getSingleTransactionStats()->getTimeActiveMicros(curTimeMicros64()),
- Microseconds{0});
-
- session.unstashTransactionResources(opCtx(), "insert");
- // The transaction machinery cannot store an empty locker.
- { Lock::GlobalLock lk(opCtx(), MODE_IX, Date_t::now(), Lock::InterruptBehavior::kThrow); }
- session.stashTransactionResources(opCtx());
-
- // Time active should have increased.
- ASSERT_GT(session.getSingleTransactionStats()->getTimeActiveMicros(curTimeMicros64()),
- Microseconds{0});
-
- // Save time active at this point.
- auto timeActiveSoFar =
- session.getSingleTransactionStats()->getTimeActiveMicros(curTimeMicros64());
-
- session.unstashTransactionResources(opCtx(), "insert");
- // Sleep here to allow enough time to elapse.
- sleepmillis(10);
- session.stashTransactionResources(opCtx());
-
- // Time active should have increased again.
- ASSERT_GT(session.getSingleTransactionStats()->getTimeActiveMicros(curTimeMicros64()),
- timeActiveSoFar);
-
- // Start a new transaction.
- const TxnNumber higherTxnNum = 2;
- session.beginOrContinueTxn(opCtx(), higherTxnNum, false, true, "testDB", "insert");
-
- // Time active should be zero for a new transaction.
- ASSERT_EQ(session.getSingleTransactionStats()->getTimeActiveMicros(curTimeMicros64()),
- Microseconds{0});
-}
-
-TEST_F(TransactionsMetricsTest, TimeActiveMicrosShouldBeSetUponUnstashAndAbort) {
- const auto sessionId = makeLogicalSessionIdForTest();
- Session session(sessionId);
- session.refreshFromStorageIfNeeded(opCtx());
-
- const TxnNumber txnNum = 1;
- opCtx()->setLogicalSessionId(sessionId);
- opCtx()->setTxnNumber(txnNum);
- session.beginOrContinueTxn(opCtx(), txnNum, false, true, "testDB", "insert");
-
- // Time active should be zero.
- ASSERT_EQ(session.getSingleTransactionStats()->getTimeActiveMicros(curTimeMicros64()),
- Microseconds{0});
-
- session.unstashTransactionResources(opCtx(), "insert");
- // Sleep here to allow enough time to elapse.
- sleepmillis(10);
- session.abortArbitraryTransaction();
-
- // Time active should have increased.
- ASSERT_GT(session.getSingleTransactionStats()->getTimeActiveMicros(curTimeMicros64()),
- Microseconds{0});
-
- // Save time active at this point.
- auto timeActiveSoFar =
- session.getSingleTransactionStats()->getTimeActiveMicros(curTimeMicros64());
-
- // The transaction is no longer active, so time active should not have increased.
- ASSERT_EQ(session.getSingleTransactionStats()->getTimeActiveMicros(curTimeMicros64()),
- timeActiveSoFar);
-}
-
-TEST_F(TransactionsMetricsTest, TimeActiveMicrosShouldNotBeSetUponAbortOnly) {
- const auto sessionId = makeLogicalSessionIdForTest();
- Session session(sessionId);
- session.refreshFromStorageIfNeeded(opCtx());
-
- const TxnNumber txnNum = 1;
- opCtx()->setLogicalSessionId(sessionId);
- opCtx()->setTxnNumber(txnNum);
- session.beginOrContinueTxn(opCtx(), txnNum, false, true, "testDB", "insert");
-
- // Time active should be zero.
- ASSERT_EQ(session.getSingleTransactionStats()->getTimeActiveMicros(curTimeMicros64()),
- Microseconds{0});
-
- session.abortArbitraryTransaction();
-
- // Time active should not have increased.
- ASSERT_EQ(session.getSingleTransactionStats()->getTimeActiveMicros(curTimeMicros64()),
- Microseconds{0});
-}
-
-TEST_F(TransactionsMetricsTest, TimeActiveMicrosShouldIncreaseUntilStash) {
- const auto sessionId = makeLogicalSessionIdForTest();
- Session session(sessionId);
- session.refreshFromStorageIfNeeded(opCtx());
-
- const TxnNumber txnNum = 1;
- opCtx()->setLogicalSessionId(sessionId);
- opCtx()->setTxnNumber(txnNum);
- session.beginOrContinueTxn(opCtx(), txnNum, false, true, "testDB", "insert");
-
- // Time active should be zero.
- ASSERT_EQ(session.getSingleTransactionStats()->getTimeActiveMicros(curTimeMicros64()),
- Microseconds{0});
- session.unstashTransactionResources(opCtx(), "insert");
- sleepmillis(1);
-
- // Time active should have increased.
- ASSERT_GT(session.getSingleTransactionStats()->getTimeActiveMicros(curTimeMicros64()),
- Microseconds{0});
-
- // Save time active at this point.
- auto timeActiveSoFar =
- session.getSingleTransactionStats()->getTimeActiveMicros(curTimeMicros64());
- sleepmillis(1);
-
- // Time active should have increased again.
- ASSERT_GT(session.getSingleTransactionStats()->getTimeActiveMicros(curTimeMicros64()),
- timeActiveSoFar);
- // The transaction machinery cannot store an empty locker.
- { Lock::GlobalLock lk(opCtx(), MODE_IX, Date_t::now(), Lock::InterruptBehavior::kThrow); }
- session.stashTransactionResources(opCtx());
-
- // The transaction is no longer active, so time active should not have increased.
- timeActiveSoFar = session.getSingleTransactionStats()->getTimeActiveMicros(curTimeMicros64());
- sleepmillis(1);
- ASSERT_EQ(session.getSingleTransactionStats()->getTimeActiveMicros(curTimeMicros64()),
- timeActiveSoFar);
-}
-
-TEST_F(TransactionsMetricsTest, TimeActiveMicrosShouldIncreaseUntilCommit) {
- const auto sessionId = makeLogicalSessionIdForTest();
- Session session(sessionId);
- session.refreshFromStorageIfNeeded(opCtx());
-
- const TxnNumber txnNum = 1;
- opCtx()->setLogicalSessionId(sessionId);
- opCtx()->setTxnNumber(txnNum);
- session.beginOrContinueTxn(opCtx(), txnNum, false, true, "admin", "commitTransaction");
-
- // Time active should be zero.
- ASSERT_EQ(session.getSingleTransactionStats()->getTimeActiveMicros(curTimeMicros64()),
- Microseconds{0});
- session.unstashTransactionResources(opCtx(), "commitTransaction");
- sleepmillis(1);
-
- // Time active should have increased.
- ASSERT_GT(session.getSingleTransactionStats()->getTimeActiveMicros(curTimeMicros64()),
- Microseconds{0});
-
- // Save time active at this point.
- auto timeActiveSoFar =
- session.getSingleTransactionStats()->getTimeActiveMicros(curTimeMicros64());
- sleepmillis(1);
-
- // Time active should have increased again.
- ASSERT_GT(session.getSingleTransactionStats()->getTimeActiveMicros(curTimeMicros64()),
- timeActiveSoFar);
- session.commitUnpreparedTransaction(opCtx());
-
- // The transaction is no longer active, so time active should not have increased.
- timeActiveSoFar = session.getSingleTransactionStats()->getTimeActiveMicros(curTimeMicros64());
- sleepmillis(1);
- ASSERT_EQ(session.getSingleTransactionStats()->getTimeActiveMicros(curTimeMicros64()),
- timeActiveSoFar);
-}
-
-TEST_F(TransactionsMetricsTest, TimeActiveMicrosShouldNotBeSetIfUnstashHasBadReadConcernArgs) {
- const auto sessionId = makeLogicalSessionIdForTest();
- Session session(sessionId);
- session.refreshFromStorageIfNeeded(opCtx());
-
- const TxnNumber txnNum = 1;
- opCtx()->setLogicalSessionId(sessionId);
- opCtx()->setTxnNumber(txnNum);
- session.beginOrContinueTxn(opCtx(), txnNum, false, true, "testDB", "find");
-
- // Initialize bad read concern args (!readConcernArgs.isEmpty()).
- repl::ReadConcernArgs readConcernArgs(repl::ReadConcernLevel::kLocalReadConcern);
- repl::ReadConcernArgs::get(opCtx()) = readConcernArgs;
-
- // Transaction resources do not exist yet.
- session.unstashTransactionResources(opCtx(), "find");
- // The transaction machinery cannot store an empty locker.
- { Lock::GlobalLock lk(opCtx(), MODE_IX, Date_t::now(), Lock::InterruptBehavior::kThrow); }
- session.stashTransactionResources(opCtx());
-
- // Time active should have increased.
- ASSERT_GT(session.getSingleTransactionStats()->getTimeActiveMicros(curTimeMicros64()),
- Microseconds{0});
-
- // Save time active at this point.
- auto timeActiveSoFar =
- session.getSingleTransactionStats()->getTimeActiveMicros(curTimeMicros64());
-
- // Transaction resources already exist here and should throw an exception due to bad read
- // concern arguments.
- ASSERT_THROWS_CODE(session.unstashTransactionResources(opCtx(), "find"),
- AssertionException,
- ErrorCodes::InvalidOptions);
-
- // Time active should not have increased.
- ASSERT_EQ(session.getSingleTransactionStats()->getTimeActiveMicros(curTimeMicros64()),
- timeActiveSoFar);
-}
-
-TEST_F(TransactionsMetricsTest, AdditiveMetricsObjectsShouldBeAddedTogetherUponStash) {
- const auto sessionId = makeLogicalSessionIdForTest();
- Session session(sessionId);
- session.refreshFromStorageIfNeeded(opCtx());
-
- const TxnNumber txnNum = 1;
- opCtx()->setLogicalSessionId(sessionId);
- opCtx()->setTxnNumber(txnNum);
- session.beginOrContinueTxn(opCtx(), txnNum, false, true, "testDB", "insert");
-
- // Initialize field values for both AdditiveMetrics objects.
- session.getSingleTransactionStats()->getOpDebug()->additiveMetrics.keysExamined = 1;
- CurOp::get(opCtx())->debug().additiveMetrics.keysExamined = 5;
- session.getSingleTransactionStats()->getOpDebug()->additiveMetrics.docsExamined = 2;
- CurOp::get(opCtx())->debug().additiveMetrics.docsExamined = 0;
- session.getSingleTransactionStats()->getOpDebug()->additiveMetrics.nMatched = 3;
- session.getSingleTransactionStats()->getOpDebug()->additiveMetrics.nModified = 1;
- CurOp::get(opCtx())->debug().additiveMetrics.nModified = 1;
- CurOp::get(opCtx())->debug().additiveMetrics.ninserted = 4;
- session.getSingleTransactionStats()->getOpDebug()->additiveMetrics.nmoved = 3;
- CurOp::get(opCtx())->debug().additiveMetrics.nmoved = 2;
- session.getSingleTransactionStats()->getOpDebug()->additiveMetrics.keysInserted = 1;
- CurOp::get(opCtx())->debug().additiveMetrics.keysInserted = 1;
- session.getSingleTransactionStats()->getOpDebug()->additiveMetrics.keysDeleted = 0;
- CurOp::get(opCtx())->debug().additiveMetrics.keysDeleted = 0;
- session.getSingleTransactionStats()->getOpDebug()->additiveMetrics.prepareReadConflicts = 5;
- CurOp::get(opCtx())->debug().additiveMetrics.prepareReadConflicts = 4;
-
- auto additiveMetricsToCompare =
- session.getSingleTransactionStats()->getOpDebug()->additiveMetrics;
- additiveMetricsToCompare.add(CurOp::get(opCtx())->debug().additiveMetrics);
-
- session.unstashTransactionResources(opCtx(), "insert");
- // The transaction machinery cannot store an empty locker.
- { Lock::GlobalLock lk(opCtx(), MODE_IX, Date_t::now(), Lock::InterruptBehavior::kThrow); }
- session.stashTransactionResources(opCtx());
-
- ASSERT(session.getSingleTransactionStats()->getOpDebug()->additiveMetrics.equals(
- additiveMetricsToCompare));
-}
-
-TEST_F(TransactionsMetricsTest, AdditiveMetricsObjectsShouldBeAddedTogetherUponCommit) {
- const auto sessionId = makeLogicalSessionIdForTest();
- Session session(sessionId);
- session.refreshFromStorageIfNeeded(opCtx());
-
- const TxnNumber txnNum = 1;
- opCtx()->setLogicalSessionId(sessionId);
- opCtx()->setTxnNumber(txnNum);
- session.beginOrContinueTxn(opCtx(), txnNum, false, true, "testDB", "insert");
-
- // Initialize field values for both AdditiveMetrics objects.
- session.getSingleTransactionStats()->getOpDebug()->additiveMetrics.keysExamined = 3;
- CurOp::get(opCtx())->debug().additiveMetrics.keysExamined = 2;
- session.getSingleTransactionStats()->getOpDebug()->additiveMetrics.docsExamined = 0;
- CurOp::get(opCtx())->debug().additiveMetrics.docsExamined = 2;
- session.getSingleTransactionStats()->getOpDebug()->additiveMetrics.nMatched = 4;
- session.getSingleTransactionStats()->getOpDebug()->additiveMetrics.nModified = 5;
- CurOp::get(opCtx())->debug().additiveMetrics.nModified = 1;
- CurOp::get(opCtx())->debug().additiveMetrics.ninserted = 1;
- session.getSingleTransactionStats()->getOpDebug()->additiveMetrics.ndeleted = 4;
- CurOp::get(opCtx())->debug().additiveMetrics.ndeleted = 0;
- session.getSingleTransactionStats()->getOpDebug()->additiveMetrics.keysInserted = 1;
- CurOp::get(opCtx())->debug().additiveMetrics.keysInserted = 1;
- session.getSingleTransactionStats()->getOpDebug()->additiveMetrics.prepareReadConflicts = 0;
- CurOp::get(opCtx())->debug().additiveMetrics.prepareReadConflicts = 0;
- session.getSingleTransactionStats()->getOpDebug()->additiveMetrics.writeConflicts = 6;
- CurOp::get(opCtx())->debug().additiveMetrics.writeConflicts = 3;
-
- auto additiveMetricsToCompare =
- session.getSingleTransactionStats()->getOpDebug()->additiveMetrics;
- additiveMetricsToCompare.add(CurOp::get(opCtx())->debug().additiveMetrics);
-
- session.unstashTransactionResources(opCtx(), "insert");
- // The transaction machinery cannot store an empty locker.
- { Lock::GlobalLock lk(opCtx(), MODE_IX, Date_t::now(), Lock::InterruptBehavior::kThrow); }
- session.commitUnpreparedTransaction(opCtx());
-
- ASSERT(session.getSingleTransactionStats()->getOpDebug()->additiveMetrics.equals(
- additiveMetricsToCompare));
-}
-
-TEST_F(TransactionsMetricsTest, AdditiveMetricsObjectsShouldBeAddedTogetherUponAbort) {
- const auto sessionId = makeLogicalSessionIdForTest();
- Session session(sessionId);
- session.refreshFromStorageIfNeeded(opCtx());
-
- const TxnNumber txnNum = 1;
- opCtx()->setLogicalSessionId(sessionId);
- opCtx()->setTxnNumber(txnNum);
- session.beginOrContinueTxn(opCtx(), txnNum, false, true, "testDB", "insert");
-
- // Initialize field values for both AdditiveMetrics objects.
- session.getSingleTransactionStats()->getOpDebug()->additiveMetrics.keysExamined = 2;
- CurOp::get(opCtx())->debug().additiveMetrics.keysExamined = 4;
- session.getSingleTransactionStats()->getOpDebug()->additiveMetrics.docsExamined = 1;
- CurOp::get(opCtx())->debug().additiveMetrics.docsExamined = 3;
- session.getSingleTransactionStats()->getOpDebug()->additiveMetrics.nMatched = 2;
- session.getSingleTransactionStats()->getOpDebug()->additiveMetrics.nModified = 0;
- CurOp::get(opCtx())->debug().additiveMetrics.nModified = 3;
- CurOp::get(opCtx())->debug().additiveMetrics.ndeleted = 5;
- session.getSingleTransactionStats()->getOpDebug()->additiveMetrics.nmoved = 0;
- CurOp::get(opCtx())->debug().additiveMetrics.nmoved = 2;
- session.getSingleTransactionStats()->getOpDebug()->additiveMetrics.keysInserted = 1;
- CurOp::get(opCtx())->debug().additiveMetrics.keysInserted = 1;
- session.getSingleTransactionStats()->getOpDebug()->additiveMetrics.keysDeleted = 6;
- CurOp::get(opCtx())->debug().additiveMetrics.keysDeleted = 0;
- session.getSingleTransactionStats()->getOpDebug()->additiveMetrics.writeConflicts = 3;
- CurOp::get(opCtx())->debug().additiveMetrics.writeConflicts = 3;
-
- auto additiveMetricsToCompare =
- session.getSingleTransactionStats()->getOpDebug()->additiveMetrics;
- additiveMetricsToCompare.add(CurOp::get(opCtx())->debug().additiveMetrics);
-
- session.unstashTransactionResources(opCtx(), "insert");
- // The transaction machinery cannot store an empty locker.
- { Lock::GlobalLock lk(opCtx(), MODE_IX, Date_t::now(), Lock::InterruptBehavior::kThrow); }
- session.abortActiveTransaction(opCtx());
-
- ASSERT(session.getSingleTransactionStats()->getOpDebug()->additiveMetrics.equals(
- additiveMetricsToCompare));
-}
-
-TEST_F(TransactionsMetricsTest, TimeInactiveMicrosShouldBeSetUponUnstashAndStash) {
- const auto sessionId = makeLogicalSessionIdForTest();
- Session session(sessionId);
- session.refreshFromStorageIfNeeded(opCtx());
-
- const TxnNumber txnNum = 1;
- opCtx()->setLogicalSessionId(sessionId);
- opCtx()->setTxnNumber(txnNum);
- session.beginOrContinueTxn(opCtx(), txnNum, false, true, "testDB", "insert");
-
- // Time inactive should be greater than or equal to zero.
- ASSERT_GTE(session.getSingleTransactionStats()->getTimeInactiveMicros(curTimeMicros64()),
- Microseconds{0});
-
- // Save time inactive at this point.
- auto timeInactiveSoFar =
- session.getSingleTransactionStats()->getTimeInactiveMicros(curTimeMicros64());
- // Sleep here to allow enough time to elapse.
- sleepmillis(1);
-
- // Time inactive should have increased.
- ASSERT_GT(session.getSingleTransactionStats()->getTimeInactiveMicros(curTimeMicros64()),
- timeInactiveSoFar);
-
- timeInactiveSoFar =
- session.getSingleTransactionStats()->getTimeInactiveMicros(curTimeMicros64());
- // Sleep here to allow enough time to elapse.
- sleepmillis(1);
-
- // The transaction is still inactive, so time inactive should have increased.
- ASSERT_GT(session.getSingleTransactionStats()->getTimeInactiveMicros(curTimeMicros64()),
- timeInactiveSoFar);
-
- session.unstashTransactionResources(opCtx(), "insert");
-
- timeInactiveSoFar =
- session.getSingleTransactionStats()->getTimeInactiveMicros(curTimeMicros64());
- // Sleep here to allow enough time to elapse.
- sleepmillis(1);
-
- // The transaction is currently active, so time inactive should not have increased.
- ASSERT_EQ(session.getSingleTransactionStats()->getTimeInactiveMicros(curTimeMicros64()),
- timeInactiveSoFar);
-
- // The transaction machinery cannot store an empty locker.
- { Lock::GlobalLock lk(opCtx(), MODE_IX, Date_t::now(), Lock::InterruptBehavior::kThrow); }
- session.stashTransactionResources(opCtx());
-
- // The transaction is inactive again, so time inactive should have increased.
- ASSERT_GT(session.getSingleTransactionStats()->getTimeInactiveMicros(curTimeMicros64()),
- timeInactiveSoFar);
-}
-
-TEST_F(TransactionsMetricsTest, TimeInactiveMicrosShouldBeSetUponUnstashAndAbort) {
- const auto sessionId = makeLogicalSessionIdForTest();
- Session session(sessionId);
- session.refreshFromStorageIfNeeded(opCtx());
-
- const TxnNumber txnNum = 1;
- opCtx()->setLogicalSessionId(sessionId);
- opCtx()->setTxnNumber(txnNum);
- session.beginOrContinueTxn(opCtx(), txnNum, false, true, "testDB", "insert");
-
- // Time inactive should be greater than or equal to zero.
- ASSERT_GTE(session.getSingleTransactionStats()->getTimeInactiveMicros(curTimeMicros64()),
- Microseconds{0});
-
- // Save time inactive at this point.
- auto timeInactiveSoFar =
- session.getSingleTransactionStats()->getTimeInactiveMicros(curTimeMicros64());
- // Sleep here to allow enough time to elapse.
- sleepmillis(1);
-
- // Time inactive should have increased.
- ASSERT_GT(session.getSingleTransactionStats()->getTimeInactiveMicros(curTimeMicros64()),
- timeInactiveSoFar);
-
- session.unstashTransactionResources(opCtx(), "insert");
- session.abortArbitraryTransaction();
-
- timeInactiveSoFar =
- session.getSingleTransactionStats()->getTimeInactiveMicros(curTimeMicros64());
- // Sleep here to allow enough time to elapse.
- sleepmillis(1);
-
- // The transaction has aborted, so time inactive should not have increased.
- ASSERT_EQ(session.getSingleTransactionStats()->getTimeInactiveMicros(curTimeMicros64()),
- timeInactiveSoFar);
-}
-
-TEST_F(TransactionsMetricsTest, TimeInactiveMicrosShouldIncreaseUntilCommit) {
- const auto sessionId = makeLogicalSessionIdForTest();
- Session session(sessionId);
- session.refreshFromStorageIfNeeded(opCtx());
-
- const TxnNumber txnNum = 1;
- opCtx()->setLogicalSessionId(sessionId);
- opCtx()->setTxnNumber(txnNum);
- session.beginOrContinueTxn(opCtx(), txnNum, false, true, "testDB", "insert");
-
- // Time inactive should be greater than or equal to zero.
- ASSERT_GTE(session.getSingleTransactionStats()->getTimeInactiveMicros(curTimeMicros64()),
- Microseconds{0});
-
- // Save time inactive at this point.
- auto timeInactiveSoFar =
- session.getSingleTransactionStats()->getTimeInactiveMicros(curTimeMicros64());
- // Sleep here to allow enough time to elapse.
- sleepmillis(1);
-
- // Time inactive should have increased.
- ASSERT_GT(session.getSingleTransactionStats()->getTimeInactiveMicros(curTimeMicros64()),
- timeInactiveSoFar);
-
- session.unstashTransactionResources(opCtx(), "insert");
- // The transaction machinery cannot store an empty locker.
- { Lock::GlobalLock lk(opCtx(), MODE_IX, Date_t::now(), Lock::InterruptBehavior::kThrow); }
- session.commitUnpreparedTransaction(opCtx());
-
- timeInactiveSoFar =
- session.getSingleTransactionStats()->getTimeInactiveMicros(curTimeMicros64());
- // Sleep here to allow enough time to elapse.
- sleepmillis(1);
-
- // The transaction has committed, so time inactive should not have increased.
- ASSERT_EQ(session.getSingleTransactionStats()->getTimeInactiveMicros(curTimeMicros64()),
- timeInactiveSoFar);
-}
-
-namespace {
-
-/*
- * Constructs a ClientMetadata BSONObj with the given application name.
- */
-BSONObj constructClientMetadata(StringData appName) {
- BSONObjBuilder builder;
- ASSERT_OK(ClientMetadata::serializePrivate("driverName",
- "driverVersion",
- "osType",
- "osName",
- "osArchitecture",
- "osVersion",
- appName,
- &builder));
- return builder.obj();
-}
-} // namespace
-
-TEST_F(TransactionsMetricsTest, LastClientInfoShouldUpdateUponStash) {
- const auto sessionId = makeLogicalSessionIdForTest();
- Session session(sessionId);
- session.refreshFromStorageIfNeeded(opCtx());
-
- const TxnNumber txnNum = 1;
- opCtx()->setLogicalSessionId(sessionId);
- opCtx()->setTxnNumber(txnNum);
-
- // Create a ClientMetadata object and set it on ClientMetadataIsMasterState.
- auto obj = constructClientMetadata("appName");
- auto clientMetadata = ClientMetadata::parse(obj["client"]);
- auto& clientMetadataIsMasterState = ClientMetadataIsMasterState::get(opCtx()->getClient());
- clientMetadataIsMasterState.setClientMetadata(opCtx()->getClient(),
- std::move(clientMetadata.getValue()));
-
- session.beginOrContinueTxn(opCtx(), txnNum, false, true, "testDB", "insert");
- session.unstashTransactionResources(opCtx(), "insert");
- // The transaction machinery cannot store an empty locker.
- { Lock::GlobalLock lk(opCtx(), MODE_IX, Date_t::now(), Lock::InterruptBehavior::kThrow); }
- session.stashTransactionResources(opCtx());
-
- // LastClientInfo should have been set.
- ASSERT_EQ(session.getSingleTransactionStats()->getLastClientInfo().clientHostAndPort, "");
- ASSERT_EQ(session.getSingleTransactionStats()->getLastClientInfo().connectionId, 0);
- ASSERT_EQ(session.getSingleTransactionStats()->getLastClientInfo().appName, "appName");
- ASSERT_BSONOBJ_EQ(session.getSingleTransactionStats()->getLastClientInfo().clientMetadata,
- obj.getField("client").Obj());
-
- // Create another ClientMetadata object.
- auto newObj = constructClientMetadata("newAppName");
- auto newClientMetadata = ClientMetadata::parse(newObj["client"]);
- clientMetadataIsMasterState.setClientMetadata(opCtx()->getClient(),
- std::move(newClientMetadata.getValue()));
-
- session.unstashTransactionResources(opCtx(), "insert");
- session.stashTransactionResources(opCtx());
-
- // LastClientInfo's clientMetadata should have been updated to the new ClientMetadata object.
- ASSERT_EQ(session.getSingleTransactionStats()->getLastClientInfo().appName, "newAppName");
- ASSERT_BSONOBJ_EQ(session.getSingleTransactionStats()->getLastClientInfo().clientMetadata,
- newObj.getField("client").Obj());
-}
-
-TEST_F(TransactionsMetricsTest, LastClientInfoShouldUpdateUponCommit) {
- const auto sessionId = makeLogicalSessionIdForTest();
- Session session(sessionId);
- session.refreshFromStorageIfNeeded(opCtx());
-
- const TxnNumber txnNum = 1;
- opCtx()->setLogicalSessionId(sessionId);
- opCtx()->setTxnNumber(txnNum);
-
- // Create a ClientMetadata object and set it on ClientMetadataIsMasterState.
- auto obj = constructClientMetadata("appName");
- auto clientMetadata = ClientMetadata::parse(obj["client"]);
- auto& clientMetadataIsMasterState = ClientMetadataIsMasterState::get(opCtx()->getClient());
- clientMetadataIsMasterState.setClientMetadata(opCtx()->getClient(),
- std::move(clientMetadata.getValue()));
-
- session.beginOrContinueTxn(opCtx(), txnNum, false, true, "testDB", "insert");
- session.unstashTransactionResources(opCtx(), "insert");
- // The transaction machinery cannot store an empty locker.
- Lock::GlobalLock lk(opCtx(), MODE_IX, Date_t::now(), Lock::InterruptBehavior::kThrow);
- session.commitUnpreparedTransaction(opCtx());
-
- // LastClientInfo should have been set.
- ASSERT_EQ(session.getSingleTransactionStats()->getLastClientInfo().clientHostAndPort, "");
- ASSERT_EQ(session.getSingleTransactionStats()->getLastClientInfo().connectionId, 0);
- ASSERT_EQ(session.getSingleTransactionStats()->getLastClientInfo().appName, "appName");
- ASSERT_BSONOBJ_EQ(session.getSingleTransactionStats()->getLastClientInfo().clientMetadata,
- obj.getField("client").Obj());
-}
-
-TEST_F(TransactionsMetricsTest, LastClientInfoShouldUpdateUponAbort) {
- const auto sessionId = makeLogicalSessionIdForTest();
- Session session(sessionId);
- session.refreshFromStorageIfNeeded(opCtx());
-
- const TxnNumber txnNum = 1;
- opCtx()->setLogicalSessionId(sessionId);
- opCtx()->setTxnNumber(txnNum);
-
- // Create a ClientMetadata object and set it on ClientMetadataIsMasterState.
- auto obj = constructClientMetadata("appName");
- auto clientMetadata = ClientMetadata::parse(obj["client"]);
-
- auto& clientMetadataIsMasterState = ClientMetadataIsMasterState::get(opCtx()->getClient());
- clientMetadataIsMasterState.setClientMetadata(opCtx()->getClient(),
- std::move(clientMetadata.getValue()));
-
- session.beginOrContinueTxn(opCtx(), txnNum, false, true, "testDB", "insert");
- session.unstashTransactionResources(opCtx(), "insert");
- session.abortActiveTransaction(opCtx());
-
- // LastClientInfo should have been set.
- ASSERT_EQ(session.getSingleTransactionStats()->getLastClientInfo().clientHostAndPort, "");
- ASSERT_EQ(session.getSingleTransactionStats()->getLastClientInfo().connectionId, 0);
- ASSERT_EQ(session.getSingleTransactionStats()->getLastClientInfo().appName, "appName");
- ASSERT_BSONOBJ_EQ(session.getSingleTransactionStats()->getLastClientInfo().clientMetadata,
- obj.getField("client").Obj());
-}
-
-namespace {
-
-/*
- * Sets up the additive metrics for Transactions Metrics test.
- */
-void setupAdditiveMetrics(const int metricValue, OperationContext* opCtx) {
- CurOp::get(opCtx)->debug().additiveMetrics.keysExamined = metricValue;
- CurOp::get(opCtx)->debug().additiveMetrics.docsExamined = metricValue;
- CurOp::get(opCtx)->debug().additiveMetrics.nMatched = metricValue;
- CurOp::get(opCtx)->debug().additiveMetrics.nModified = metricValue;
- CurOp::get(opCtx)->debug().additiveMetrics.ninserted = metricValue;
- CurOp::get(opCtx)->debug().additiveMetrics.ndeleted = metricValue;
- CurOp::get(opCtx)->debug().additiveMetrics.nmoved = metricValue;
- CurOp::get(opCtx)->debug().additiveMetrics.keysInserted = metricValue;
- CurOp::get(opCtx)->debug().additiveMetrics.keysDeleted = metricValue;
- CurOp::get(opCtx)->debug().additiveMetrics.prepareReadConflicts = metricValue;
- CurOp::get(opCtx)->debug().additiveMetrics.writeConflicts = metricValue;
-}
-
-/*
- * Builds expected parameters info string.
- */
-void buildParametersInfoString(StringBuilder* sb,
- LogicalSessionId sessionId,
- const TxnNumber txnNum,
- const repl::ReadConcernArgs readConcernArgs) {
- BSONObjBuilder lsidBuilder;
- sessionId.serialize(&lsidBuilder);
- (*sb) << "parameters:{ lsid: " << lsidBuilder.done().toString() << ", txnNumber: " << txnNum
- << ", autocommit: false"
- << ", readConcern: " << readConcernArgs.toBSON().getObjectField("readConcern") << " },";
-}
-
-/*
- * Builds expected single transaction stats info string.
- */
-void buildSingleTransactionStatsString(StringBuilder* sb, const int metricValue) {
- (*sb) << " keysExamined:" << metricValue << " docsExamined:" << metricValue
- << " nMatched:" << metricValue << " nModified:" << metricValue
- << " ninserted:" << metricValue << " ndeleted:" << metricValue
- << " nmoved:" << metricValue << " keysInserted:" << metricValue
- << " keysDeleted:" << metricValue << " prepareReadConflicts:" << metricValue
- << " writeConflicts:" << metricValue;
-}
-
-/*
- * Builds the time active and time inactive info string.
- */
-void buildTimeActiveInactiveString(StringBuilder* sb,
- Session* session,
- unsigned long long curTime) {
- // Add time active micros to string.
- (*sb) << " timeActiveMicros:"
- << durationCount<Microseconds>(
- session->getSingleTransactionStats()->getTimeActiveMicros(curTime));
-
- // Add time inactive micros to string.
- (*sb) << " timeInactiveMicros:"
- << durationCount<Microseconds>(
- session->getSingleTransactionStats()->getTimeInactiveMicros(curTime));
-}
-
-
-/*
- * Builds the entire expected transaction info string and returns it.
- */
-std::string buildTransactionInfoString(OperationContext* opCtx,
- Session* session,
- std::string terminationCause,
- const LogicalSessionId sessionId,
- const TxnNumber txnNum,
- const int metricValue) {
- // Calling transactionInfoForLog to get the actual transaction info string.
- const auto lockerInfo = opCtx->lockState()->getLockerInfo();
-
- // Building expected transaction info string.
- StringBuilder parametersInfo;
- buildParametersInfoString(
- &parametersInfo, sessionId, txnNum, repl::ReadConcernArgs::get(opCtx));
-
- StringBuilder readTimestampInfo;
- readTimestampInfo
- << " readTimestamp:"
- << session->getSpeculativeTransactionReadOpTimeForTest().getTimestamp().toString() << ",";
-
- StringBuilder singleTransactionStatsInfo;
- buildSingleTransactionStatsString(&singleTransactionStatsInfo, metricValue);
-
- auto curTime = curTimeMicros64();
- StringBuilder timeActiveAndInactiveInfo;
- buildTimeActiveInactiveString(&timeActiveAndInactiveInfo, session, curTime);
-
- BSONObjBuilder locks;
- if (lockerInfo) {
- lockerInfo->stats.report(&locks);
- }
-
- // Puts all the substrings together into one expected info string. The expected info string will
- // look something like this:
- // parameters:{ lsid: { id: UUID("f825288c-100e-49a1-9fd7-b95c108049e6"), uid: BinData(0,
- // E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855) }, txnNumber: 1,
- // autocommit: false }, readTimestamp:Timestamp(0, 0), keysExamined:1 docsExamined:1 nMatched:1
- // nModified:1 ninserted:1 ndeleted:1 nmoved:1 keysInserted:1 keysDeleted:1
- // prepareReadConflicts:1 writeConflicts:1 terminationCause:committed timeActiveMicros:3
- // timeInactiveMicros:2 numYields:0 locks:{ Global: { acquireCount: { r: 6, w: 4 } }, Database:
- // { acquireCount: { r: 1, w: 1, W: 2 } }, Collection: { acquireCount: { R: 1 } }, oplog: {
- // acquireCount: { W: 1 } } } 0ms
- StringBuilder expectedTransactionInfo;
- expectedTransactionInfo << parametersInfo.str() << readTimestampInfo.str()
- << singleTransactionStatsInfo.str()
- << " terminationCause:" << terminationCause
- << timeActiveAndInactiveInfo.str() << " numYields:" << 0
- << " locks:" << locks.done().toString() << " "
- << Milliseconds{
- static_cast<long long>(
- session->getSingleTransactionStats()->getDuration(curTime)) /
- 1000};
- return expectedTransactionInfo.str();
-}
-} // namespace
-
-TEST_F(TransactionsMetricsTest, TestTransactionInfoForLogAfterCommit) {
- const auto sessionId = makeLogicalSessionIdForTest();
- Session session(sessionId);
- session.refreshFromStorageIfNeeded(opCtx());
-
- const TxnNumber txnNum = 1;
- opCtx()->setLogicalSessionId(sessionId);
- opCtx()->setTxnNumber(txnNum);
-
- repl::ReadConcernArgs readConcernArgs;
- ASSERT_OK(readConcernArgs.initialize(BSON("find"
- << "test"
- << repl::ReadConcernArgs::kReadConcernFieldName
- << BSON(repl::ReadConcernArgs::kLevelFieldName
- << "snapshot"))));
- repl::ReadConcernArgs::get(opCtx()) = readConcernArgs;
-
- session.beginOrContinueTxn(opCtx(), txnNum, false, true, "admin", "commitTransaction");
-
- // Initialize SingleTransactionStats AdditiveMetrics objects.
- const int metricValue = 1;
- setupAdditiveMetrics(metricValue, opCtx());
-
- session.unstashTransactionResources(opCtx(), "commitTransaction");
- session.commitUnpreparedTransaction(opCtx());
-
- const auto lockerInfo = opCtx()->lockState()->getLockerInfo();
- ASSERT(lockerInfo);
- std::string testTransactionInfo =
- session.transactionInfoForLogForTest(&lockerInfo->stats, true, readConcernArgs);
-
- std::string expectedTransactionInfo =
- buildTransactionInfoString(opCtx(), &session, "committed", sessionId, txnNum, metricValue);
- ASSERT_EQ(testTransactionInfo, expectedTransactionInfo);
-}
-
-TEST_F(TransactionsMetricsTest, TestTransactionInfoForLogAfterAbort) {
- const auto sessionId = makeLogicalSessionIdForTest();
- Session session(sessionId);
- session.refreshFromStorageIfNeeded(opCtx());
-
- const TxnNumber txnNum = 1;
- opCtx()->setLogicalSessionId(sessionId);
- opCtx()->setTxnNumber(txnNum);
-
- repl::ReadConcernArgs readConcernArgs;
- ASSERT_OK(readConcernArgs.initialize(BSON("find"
- << "test"
- << repl::ReadConcernArgs::kReadConcernFieldName
- << BSON(repl::ReadConcernArgs::kLevelFieldName
- << "snapshot"))));
- repl::ReadConcernArgs::get(opCtx()) = readConcernArgs;
-
- session.beginOrContinueTxn(opCtx(), txnNum, false, true, "admin", "abortTransaction");
-
- // Initialize SingleTransactionStats AdditiveMetrics objects.
- const int metricValue = 1;
- setupAdditiveMetrics(metricValue, opCtx());
-
- session.unstashTransactionResources(opCtx(), "abortTransaction");
- session.abortActiveTransaction(opCtx());
-
- const auto lockerInfo = opCtx()->lockState()->getLockerInfo();
- ASSERT(lockerInfo);
- std::string testTransactionInfo =
- session.transactionInfoForLogForTest(&lockerInfo->stats, false, readConcernArgs);
-
- std::string expectedTransactionInfo =
- buildTransactionInfoString(opCtx(), &session, "aborted", sessionId, txnNum, metricValue);
- ASSERT_EQ(testTransactionInfo, expectedTransactionInfo);
-}
-
-DEATH_TEST_F(TransactionsMetricsTest, TestTransactionInfoForLogWithNoLockerInfoStats, "invariant") {
- const auto sessionId = makeLogicalSessionIdForTest();
- Session session(sessionId);
- session.refreshFromStorageIfNeeded(opCtx());
-
- const TxnNumber txnNum = 1;
- opCtx()->setLogicalSessionId(sessionId);
- opCtx()->setTxnNumber(txnNum);
-
- repl::ReadConcernArgs readConcernArgs;
- ASSERT_OK(readConcernArgs.initialize(BSON("find"
- << "test"
- << repl::ReadConcernArgs::kReadConcernFieldName
- << BSON(repl::ReadConcernArgs::kLevelFieldName
- << "snapshot"))));
- repl::ReadConcernArgs::get(opCtx()) = readConcernArgs;
-
- session.beginOrContinueTxn(opCtx(), txnNum, false, true, "testDB", "insert");
-
- session.unstashTransactionResources(opCtx(), "commitTransaction");
- session.commitUnpreparedTransaction(opCtx());
-
- session.transactionInfoForLogForTest(nullptr, true, readConcernArgs);
-}
-
-TEST_F(TransactionsMetricsTest, LogTransactionInfoAfterSlowCommit) {
- const auto sessionId = makeLogicalSessionIdForTest();
- Session session(sessionId);
- session.refreshFromStorageIfNeeded(opCtx());
-
- const TxnNumber txnNum = 1;
- opCtx()->setLogicalSessionId(sessionId);
- opCtx()->setTxnNumber(txnNum);
-
- repl::ReadConcernArgs readConcernArgs;
- ASSERT_OK(readConcernArgs.initialize(BSON("find"
- << "test"
- << repl::ReadConcernArgs::kReadConcernFieldName
- << BSON(repl::ReadConcernArgs::kLevelFieldName
- << "snapshot"))));
- repl::ReadConcernArgs::get(opCtx()) = readConcernArgs;
-
- session.beginOrContinueTxn(opCtx(), txnNum, false, true, "admin", "commitTransaction");
- // Initialize SingleTransactionStats AdditiveMetrics objects.
- const int metricValue = 1;
- setupAdditiveMetrics(metricValue, opCtx());
-
- session.unstashTransactionResources(opCtx(), "commitTransaction");
-
- serverGlobalParams.slowMS = 10;
- sleepmillis(serverGlobalParams.slowMS + 1);
-
- startCapturingLogMessages();
- session.commitUnpreparedTransaction(opCtx());
- stopCapturingLogMessages();
-
- const auto lockerInfo = opCtx()->lockState()->getLockerInfo();
- ASSERT(lockerInfo);
- std::string expectedTransactionInfo = "transaction " +
- session.transactionInfoForLogForTest(&lockerInfo->stats, true, readConcernArgs);
- ASSERT_EQUALS(1, countLogLinesContaining(expectedTransactionInfo));
-}
-
-TEST_F(TransactionsMetricsTest, LogTransactionInfoAfterSlowAbort) {
- const auto sessionId = makeLogicalSessionIdForTest();
- Session session(sessionId);
- session.refreshFromStorageIfNeeded(opCtx());
-
- const TxnNumber txnNum = 1;
- opCtx()->setLogicalSessionId(sessionId);
- opCtx()->setTxnNumber(txnNum);
-
- repl::ReadConcernArgs readConcernArgs;
- ASSERT_OK(readConcernArgs.initialize(BSON("find"
- << "test"
- << repl::ReadConcernArgs::kReadConcernFieldName
- << BSON(repl::ReadConcernArgs::kLevelFieldName
- << "snapshot"))));
- repl::ReadConcernArgs::get(opCtx()) = readConcernArgs;
-
- session.beginOrContinueTxn(opCtx(), txnNum, false, true, "admin", "abortTransaction");
- // Initialize SingleTransactionStats AdditiveMetrics objects.
- const int metricValue = 1;
- setupAdditiveMetrics(metricValue, opCtx());
-
- session.unstashTransactionResources(opCtx(), "abortTransaction");
-
- serverGlobalParams.slowMS = 10;
- sleepmillis(serverGlobalParams.slowMS + 1);
-
- startCapturingLogMessages();
- session.abortActiveTransaction(opCtx());
- stopCapturingLogMessages();
-
- const auto lockerInfo = opCtx()->lockState()->getLockerInfo();
- ASSERT(lockerInfo);
- std::string expectedTransactionInfo = "transaction " +
- session.transactionInfoForLogForTest(&lockerInfo->stats, false, readConcernArgs);
- ASSERT_EQUALS(1, countLogLinesContaining(expectedTransactionInfo));
-}
-
-TEST_F(TransactionsMetricsTest, LogTransactionInfoAfterSlowStashedAbort) {
- const auto sessionId = makeLogicalSessionIdForTest();
- Session session(sessionId);
- session.refreshFromStorageIfNeeded(opCtx());
-
- const TxnNumber txnNum = 1;
- opCtx()->setLogicalSessionId(sessionId);
- opCtx()->setTxnNumber(txnNum);
-
- repl::ReadConcernArgs readConcernArgs;
- ASSERT_OK(readConcernArgs.initialize(BSON("find"
- << "test"
- << repl::ReadConcernArgs::kReadConcernFieldName
- << BSON(repl::ReadConcernArgs::kLevelFieldName
- << "snapshot"))));
- repl::ReadConcernArgs::get(opCtx()) = readConcernArgs;
-
- session.beginOrContinueTxn(opCtx(), txnNum, false, true, "admin", "abortTransaction");
- // Initialize SingleTransactionStats AdditiveMetrics objects.
- const int metricValue = 1;
- setupAdditiveMetrics(metricValue, opCtx());
-
- session.unstashTransactionResources(opCtx(), "insert");
- { Lock::GlobalLock lk(opCtx(), MODE_IX, Date_t::now(), Lock::InterruptBehavior::kThrow); }
- session.stashTransactionResources(opCtx());
- const auto txnResourceStashLocker = session.getTxnResourceStashLockerForTest();
- ASSERT(txnResourceStashLocker);
- const auto lockerInfo = txnResourceStashLocker->getLockerInfo();
-
- serverGlobalParams.slowMS = 10;
- sleepmillis(serverGlobalParams.slowMS + 1);
-
- startCapturingLogMessages();
- session.abortArbitraryTransaction();
- stopCapturingLogMessages();
-
- std::string expectedTransactionInfo = "transaction " +
- session.transactionInfoForLogForTest(&lockerInfo->stats, false, readConcernArgs);
- ASSERT_EQUALS(1, countLogLinesContaining(expectedTransactionInfo));
-}
-
} // namespace
} // namespace mongo
diff --git a/src/mongo/db/transaction_participant.cpp b/src/mongo/db/transaction_participant.cpp
index 657b24fa0d6..35238c34909 100644
--- a/src/mongo/db/transaction_participant.cpp
+++ b/src/mongo/db/transaction_participant.cpp
@@ -26,32 +26,1069 @@
* it in the license file.
*/
-#define MONGO_LOG_DEFAULT_COMPONENT ::mongo::logger::LogComponent::kSharding
+#define MONGO_LOG_DEFAULT_COMPONENT ::mongo::logger::LogComponent::kStorage
#include "mongo/platform/basic.h"
#include "mongo/db/transaction_participant.h"
+#include "mongo/db/commands/test_commands_enabled.h"
+#include "mongo/db/concurrency/d_concurrency.h"
+#include "mongo/db/concurrency/lock_state.h"
+#include "mongo/db/concurrency/locker.h"
+#include "mongo/db/op_observer.h"
+#include "mongo/db/repl/repl_client_info.h"
+#include "mongo/db/server_parameters.h"
+#include "mongo/db/server_transactions_metrics.h"
#include "mongo/db/session.h"
+#include "mongo/db/session_catalog.h"
+#include "mongo/db/stats/fill_locker_info.h"
+#include "mongo/util/fail_point_service.h"
+#include "mongo/util/log.h"
+#include "mongo/util/net/socket_utils.h"
namespace mongo {
using Action = TransactionParticipant::StateMachine::Action;
using Event = TransactionParticipant::StateMachine::Event;
using State = TransactionParticipant::StateMachine::State;
+// Server parameter that dictates the max number of milliseconds that any transaction lock request
+// will wait for lock acquisition. If an operation provides a greater timeout in a lock request,
+// maxTransactionLockRequestTimeoutMillis will override it. If this is set to a negative value, it
+// is inactive and nothing will be overridden.
+//
+// 5 milliseconds will help avoid deadlocks, but will still allow fast-running metadata operations
+// to run without aborting transactions.
+MONGO_EXPORT_SERVER_PARAMETER(maxTransactionLockRequestTimeoutMillis, int, 5);
+
+// Server parameter that dictates the lifetime given to each transaction.
+// Transactions must eventually expire to preempt storage cache pressure immobilizing the system.
+MONGO_EXPORT_SERVER_PARAMETER(transactionLifetimeLimitSeconds, std::int32_t, 60)
+ ->withValidator([](const auto& potentialNewValue) {
+ if (potentialNewValue < 1) {
+ return Status(ErrorCodes::BadValue,
+ "transactionLifetimeLimitSeconds must be greater than or equal to 1s");
+ }
+
+ return Status::OK();
+ });
namespace {
-const Session::Decoration<boost::optional<TransactionParticipant>> getTransactionParticipant =
- Session::declareDecoration<boost::optional<TransactionParticipant>>();
-} // namespace
-boost::optional<TransactionParticipant>& TransactionParticipant::get(Session* session) {
- return getTransactionParticipant(session);
+// Failpoint which will pause an operation just after allocating a point-in-time storage engine
+// transaction.
+MONGO_FAIL_POINT_DEFINE(hangAfterPreallocateSnapshot);
+
+MONGO_FAIL_POINT_DEFINE(hangAfterReservingPrepareTimestamp);
+
+const auto getTransactionParticipant = Session::declareDecoration<TransactionParticipant>();
+
+// The command names that are allowed in a multi-document transaction.
+const StringMap<int> txnCmdWhitelist = {{"abortTransaction", 1},
+ {"aggregate", 1},
+ {"commitTransaction", 1},
+ {"coordinateCommitTransaction", 1},
+ {"delete", 1},
+ {"distinct", 1},
+ {"doTxn", 1},
+ {"find", 1},
+ {"findandmodify", 1},
+ {"findAndModify", 1},
+ {"geoSearch", 1},
+ {"getMore", 1},
+ {"insert", 1},
+ {"killCursors", 1},
+ {"prepareTransaction", 1},
+ {"update", 1}};
+
+// The command names that are allowed in a multi-document transaction only when test commands are
+// enabled.
+const StringMap<int> txnCmdForTestingWhitelist = {{"dbHash", 1}};
+
+// The commands that can be run on the 'admin' database in multi-document transactions.
+const StringMap<int> txnAdminCommands = {{"abortTransaction", 1},
+ {"commitTransaction", 1},
+ {"coordinateCommitTransaction", 1},
+ {"doTxn", 1},
+ {"prepareTransaction", 1}};
+
+} // unnamed namespace
+
+TransactionParticipant* TransactionParticipant::get(OperationContext* opCtx) {
+ auto session = OperationContextSession::get(opCtx);
+ if (!session) {
+ return nullptr;
+ }
+
+ return &getTransactionParticipant(session);
+}
+
+TransactionParticipant* TransactionParticipant::getFromNonCheckedOutSession(Session* session) {
+ return &getTransactionParticipant(session);
+}
+
+const Session* TransactionParticipant::_getSession() const {
+ return getTransactionParticipant.owner(this);
+}
+
+Session* TransactionParticipant::_getSession() {
+ return getTransactionParticipant.owner(this);
+}
+
+void TransactionParticipant::beginOrContinue(TxnNumber txnNumber,
+ boost::optional<bool> autocommit,
+ boost::optional<bool> startTransaction) {
+ stdx::lock_guard<stdx::mutex> lg(_mutex);
+
+ if (auto newState = _getSession()->getLastRefreshState()) {
+ _updateState(lg, *newState);
+ }
+
+ if (txnNumber == _activeTxnNumber) {
+ // It is never valid to specify 'startTransaction' on an active transaction.
+ uassert(ErrorCodes::ConflictingOperationInProgress,
+ str::stream() << "Cannot specify 'startTransaction' on transaction " << txnNumber
+ << " since it is already in progress.",
+ startTransaction == boost::none);
+
+ if (_txnState.isNone(lg)) {
+ uassert(ErrorCodes::InvalidOptions,
+ "Cannot specify 'autocommit' on an operation not inside a multi-statement "
+ "transaction.",
+ autocommit == boost::none);
+ return;
+ }
+
+ // Continue a multi-statement transaction. In this case, it is required that
+ // autocommit=false be given as an argument on the request.
+
+ uassert(ErrorCodes::InvalidOptions,
+ "Must specify autocommit=false on all operations of a multi-statement transaction.",
+ autocommit == boost::optional<bool>(false));
+
+ if (_txnState.isInProgress(lg) && !_txnResourceStash) {
+ // This indicates that the first command in the transaction failed but did not
+ // implicitly abort the transaction. It is not safe to continue the transaction, in
+ // particular because we have not saved the readConcern from the first statement of
+ // the transaction.
+ _abortTransactionOnSession(lg);
+ uasserted(ErrorCodes::NoSuchTransaction,
+ str::stream() << "Transaction " << txnNumber << " has been aborted.");
+ }
+
+ return;
+ }
+
+ if (autocommit) {
+ uassert(ErrorCodes::NoSuchTransaction,
+ str::stream() << "Given transaction number " << txnNumber
+ << " does not match any in-progress transactions.",
+ startTransaction != boost::none);
+
+ // We cannot start a transaction if a prepared transaction is already running on the
+ // session.
+ uassert(ErrorCodes::PreparedTransactionInProgress,
+ "Cannot start a new transaction when a prepared transaction already exists on the "
+ "session.",
+ !_txnState.isPrepared(lg));
+ }
+
+ _setNewTxnNumber(lg, txnNumber);
+
+ _autoCommit = autocommit;
+ if (!autocommit) {
+ return;
+ }
+
+ // Start a multi-document transaction.
+ invariant(*autocommit == false);
+ _txnState.transitionTo(lg, TransactionState::kInProgress);
+
+ // Tracks various transactions metrics.
+ _singleTransactionStats.setStartTime(curTimeMicros64());
+ _transactionExpireDate =
+ Date_t::fromMillisSinceEpoch(_singleTransactionStats.getStartTime() / 1000) +
+ stdx::chrono::seconds{transactionLifetimeLimitSeconds.load()};
+
+ ServerTransactionsMetrics::get(getGlobalServiceContext())->incrementTotalStarted();
+ ServerTransactionsMetrics::get(getGlobalServiceContext())->incrementCurrentOpen();
+
+ invariant(_transactionOperations.empty());
+}
+
+void TransactionParticipant::setSpeculativeTransactionOpTimeToLastApplied(OperationContext* opCtx) {
+ stdx::lock_guard<stdx::mutex> lg(_mutex);
+ repl::ReplicationCoordinator* replCoord =
+ repl::ReplicationCoordinator::get(opCtx->getClient()->getServiceContext());
+ opCtx->recoveryUnit()->setTimestampReadSource(RecoveryUnit::ReadSource::kLastAppliedSnapshot);
+ opCtx->recoveryUnit()->preallocateSnapshot();
+ auto readTimestamp = opCtx->recoveryUnit()->getPointInTimeReadTimestamp();
+ invariant(readTimestamp);
+ // Transactions do not survive term changes, so combining "getTerm" here with the
+ // recovery unit timestamp does not cause races.
+ _speculativeTransactionReadOpTime = {*readTimestamp, replCoord->getTerm()};
+}
+
+TransactionParticipant::OplogSlotReserver::OplogSlotReserver(OperationContext* opCtx) {
+ // Stash the transaction on the OperationContext on the stack. At the end of this function it
+ // will be unstashed onto the OperationContext.
+ TransactionParticipant::SideTransactionBlock sideTxn(opCtx);
+
+ // Begin a new WUOW and reserve a slot in the oplog.
+ WriteUnitOfWork wuow(opCtx);
+ _oplogSlot = repl::getNextOpTime(opCtx);
+
+ // Release the WUOW state since this WUOW is no longer in use.
+ wuow.release();
+
+ // The new transaction should have an empty locker, and thus we do not need to save it.
+ invariant(opCtx->lockState()->getClientState() == Locker::ClientState::kInactive);
+ _locker = opCtx->swapLockState(stdx::make_unique<LockerImpl>());
+ _locker->unsetThreadId();
+
+ // This thread must still respect the transaction lock timeout, since it can prevent the
+ // transaction from making progress.
+ auto maxTransactionLockMillis = maxTransactionLockRequestTimeoutMillis.load();
+ if (maxTransactionLockMillis >= 0) {
+ opCtx->lockState()->setMaxLockTimeout(Milliseconds(maxTransactionLockMillis));
+ }
+
+ // Save the RecoveryUnit from the new transaction and replace it with an empty one.
+ _recoveryUnit = std::unique_ptr<RecoveryUnit>(opCtx->releaseRecoveryUnit());
+ opCtx->setRecoveryUnit(opCtx->getServiceContext()->getStorageEngine()->newRecoveryUnit(),
+ WriteUnitOfWork::RecoveryUnitState::kNotInUnitOfWork);
+}
+
+TransactionParticipant::OplogSlotReserver::~OplogSlotReserver() {
+ // If the constructor did not complete, we do not attempt to abort the units of work.
+ if (_recoveryUnit) {
+ // We should be at WUOW nesting level 1, only the top level WUOW for the oplog reservation
+ // side transaction.
+ _locker->endWriteUnitOfWork();
+ invariant(!_locker->inAWriteUnitOfWork());
+ _recoveryUnit->abortUnitOfWork();
+ }
+}
+
+TransactionParticipant::TxnResources::TxnResources(OperationContext* opCtx, bool keepTicket) {
+ _ruState = opCtx->getWriteUnitOfWork()->release();
+ opCtx->setWriteUnitOfWork(nullptr);
+
+ _locker = opCtx->swapLockState(stdx::make_unique<LockerImpl>());
+ if (!keepTicket) {
+ _locker->releaseTicket();
+ }
+ _locker->unsetThreadId();
+
+ // This thread must still respect the transaction lock timeout, since it can prevent the
+ // transaction from making progress.
+ auto maxTransactionLockMillis = maxTransactionLockRequestTimeoutMillis.load();
+ if (maxTransactionLockMillis >= 0) {
+ opCtx->lockState()->setMaxLockTimeout(Milliseconds(maxTransactionLockMillis));
+ }
+
+ _recoveryUnit = std::unique_ptr<RecoveryUnit>(opCtx->releaseRecoveryUnit());
+ opCtx->setRecoveryUnit(opCtx->getServiceContext()->getStorageEngine()->newRecoveryUnit(),
+ WriteUnitOfWork::RecoveryUnitState::kNotInUnitOfWork);
+
+ _readConcernArgs = repl::ReadConcernArgs::get(opCtx);
+}
+
+TransactionParticipant::TxnResources::~TxnResources() {
+ if (!_released && _recoveryUnit) {
+ // This should only be reached when aborting a transaction that isn't active, i.e.
+ // when starting a new transaction before completing an old one. So we should
+ // be at WUOW nesting level 1 (only the top level WriteUnitOfWork).
+ _locker->endWriteUnitOfWork();
+ invariant(!_locker->inAWriteUnitOfWork());
+ _recoveryUnit->abortUnitOfWork();
+ }
+}
+
+void TransactionParticipant::TxnResources::release(OperationContext* opCtx) {
+ // Perform operations that can fail the release before marking the TxnResources as released.
+ _locker->reacquireTicket(opCtx);
+
+ invariant(!_released);
+ _released = true;
+
+ // We intentionally do not capture the return value of swapLockState(), which is just an empty
+ // locker. At the end of the operation, if the transaction is not complete, we will stash the
+ // operation context's locker and replace it with a new empty locker.
+ invariant(opCtx->lockState()->getClientState() == Locker::ClientState::kInactive);
+ opCtx->swapLockState(std::move(_locker));
+ opCtx->lockState()->updateThreadIdToCurrentThread();
+
+ auto oldState = opCtx->setRecoveryUnit(_recoveryUnit.release(),
+ WriteUnitOfWork::RecoveryUnitState::kNotInUnitOfWork);
+ invariant(oldState == WriteUnitOfWork::RecoveryUnitState::kNotInUnitOfWork,
+ str::stream() << "RecoveryUnit state was " << oldState);
+
+ opCtx->setWriteUnitOfWork(WriteUnitOfWork::createForSnapshotResume(opCtx, _ruState));
+
+ auto& readConcernArgs = repl::ReadConcernArgs::get(opCtx);
+ readConcernArgs = _readConcernArgs;
+}
+
+TransactionParticipant::SideTransactionBlock::SideTransactionBlock(OperationContext* opCtx)
+ : _opCtx(opCtx) {
+ if (_opCtx->getWriteUnitOfWork()) {
+ // This must be done under the client lock, since we are modifying '_opCtx'.
+ stdx::lock_guard<Client> clientLock(*_opCtx->getClient());
+ _txnResources = TransactionParticipant::TxnResources(_opCtx, true /* keepTicket*/);
+ }
+}
+
+TransactionParticipant::SideTransactionBlock::~SideTransactionBlock() {
+ if (_txnResources) {
+ // Restore the transaction state onto '_opCtx'. This must be done under the
+ // client lock, since we are modifying '_opCtx'.
+ stdx::lock_guard<Client> clientLock(*_opCtx->getClient());
+ _txnResources->release(_opCtx);
+ }
+}
+
+void TransactionParticipant::stashTransactionResources(OperationContext* opCtx) {
+ if (opCtx->getClient()->isInDirectClient()) {
+ return;
+ }
+
+ invariant(opCtx->getTxnNumber());
+
+ // We must lock the Client to change the Locker on the OperationContext and the Session mutex to
+ // access Session state. We must lock the Client before the Session mutex, since the Client
+ // effectively owns the Session. That is, a user might lock the Client to ensure it doesn't go
+ // away, and then lock the Session owned by that client. We rely on the fact that we are not
+ // using the DefaultLockerImpl to avoid deadlock.
+ stdx::lock_guard<Client> lk(*opCtx->getClient());
+ stdx::unique_lock<stdx::mutex> lg(_mutex);
+
+ // Always check session's txnNumber, since it can be modified by migration, which does not
+ // check out the session. We intentionally do not error if _txnState=kAborted, since we
+ // expect this function to be called at the end of the 'abortTransaction' command.
+ _checkIsActiveTransaction(lg, *opCtx->getTxnNumber(), false);
+
+ if (!_txnState.inMultiDocumentTransaction(lg)) {
+ // Not in a multi-document transaction: nothing to do.
+ return;
+ }
+
+ if (_singleTransactionStats.isActive()) {
+ _singleTransactionStats.setInactive(curTimeMicros64());
+ }
+
+ // Add the latest operation stats to the aggregate OpDebug object stored in the
+ // SingleTransactionStats instance on the Session.
+ _singleTransactionStats.getOpDebug()->additiveMetrics.add(
+ CurOp::get(opCtx)->debug().additiveMetrics);
+
+ invariant(!_txnResourceStash);
+ _txnResourceStash = TxnResources(opCtx);
+
+ // We accept possible slight inaccuracies in these counters from non-atomicity.
+ ServerTransactionsMetrics::get(opCtx)->decrementCurrentActive();
+ ServerTransactionsMetrics::get(opCtx)->incrementCurrentInactive();
+
+ // Update the LastClientInfo object stored in the SingleTransactionStats instance on the Session
+ // with this Client's information. This is the last client that ran a transaction operation on
+ // the Session.
+ _singleTransactionStats.updateLastClientInfo(opCtx->getClient());
+}
+
+void TransactionParticipant::unstashTransactionResources(OperationContext* opCtx,
+ const std::string& cmdName) {
+ if (opCtx->getClient()->isInDirectClient()) {
+ return;
+ }
+
+ invariant(opCtx->getTxnNumber());
+
+ {
+ // We must lock the Client to change the Locker on the OperationContext and the Session
+ // mutex to access Session state. We must lock the Client before the Session mutex, since
+ // the Client effectively owns the Session. That is, a user might lock the Client to ensure
+ // it doesn't go away, and then lock the Session owned by that client.
+ stdx::lock_guard<Client> lk(*opCtx->getClient());
+ stdx::lock_guard<stdx::mutex> lg(_mutex);
+
+ // Always check session's txnNumber and '_txnState', since they can be modified by session
+ // kill and migration, which do not check out the session.
+ _checkIsActiveTransaction(lg, *opCtx->getTxnNumber(), false);
+
+ // If this is not a multi-document transaction, there is nothing to unstash.
+ if (_txnState.isNone(lg)) {
+ invariant(!_txnResourceStash);
+ return;
+ }
+
+ // Throw NoSuchTransaction error instead of TransactionAborted error since this is the entry
+ // point of transaction execution.
+ uassert(ErrorCodes::NoSuchTransaction,
+ str::stream() << "Transaction " << *opCtx->getTxnNumber() << " has been aborted.",
+ !_txnState.isAborted(lg));
+
+ // Cannot change committed transaction but allow retrying commitTransaction command.
+ uassert(ErrorCodes::TransactionCommitted,
+ str::stream() << "Transaction " << *opCtx->getTxnNumber() << " has been committed.",
+ cmdName == "commitTransaction" || !_txnState.isCommitted(lg));
+
+ if (_txnResourceStash) {
+ // Transaction resources already exist for this transaction. Transfer them from the
+ // stash to the operation context.
+
+ auto& readConcernArgs = repl::ReadConcernArgs::get(opCtx);
+ uassert(ErrorCodes::InvalidOptions,
+ "Only the first command in a transaction may specify a readConcern",
+ readConcernArgs.isEmpty());
+ _txnResourceStash->release(opCtx);
+ _txnResourceStash = boost::none;
+ // Set the starting active time for this transaction.
+ if (_txnState.isInProgress(lk)) {
+ _singleTransactionStats.setActive(curTimeMicros64());
+ }
+ // We accept possible slight inaccuracies in these counters from non-atomicity.
+ ServerTransactionsMetrics::get(opCtx)->incrementCurrentActive();
+ ServerTransactionsMetrics::get(opCtx)->decrementCurrentInactive();
+ return;
+ }
+
+ // If we have no transaction resources then we cannot be prepared. If we're not in progress,
+ // we don't do anything else.
+ invariant(!_txnState.isPrepared(lk));
+ if (!_txnState.isInProgress(lg)) {
+ // At this point we're either committed and this is a 'commitTransaction' command, or we
+ // are in the process of committing.
+ return;
+ }
+
+ // Stashed transaction resources do not exist for this in-progress multi-document
+ // transaction. Set up the transaction resources on the opCtx.
+ opCtx->setWriteUnitOfWork(std::make_unique<WriteUnitOfWork>(opCtx));
+ ServerTransactionsMetrics::get(getGlobalServiceContext())->incrementCurrentActive();
+
+ // Set the starting active time for this transaction.
+ _singleTransactionStats.setActive(curTimeMicros64());
+
+ // If maxTransactionLockRequestTimeoutMillis is set, then we will ensure no
+ // future lock request waits longer than maxTransactionLockRequestTimeoutMillis
+ // to acquire a lock. This is to avoid deadlocks and minimize non-transaction
+ // operation performance degradations.
+ auto maxTransactionLockMillis = maxTransactionLockRequestTimeoutMillis.load();
+ if (maxTransactionLockMillis >= 0) {
+ opCtx->lockState()->setMaxLockTimeout(Milliseconds(maxTransactionLockMillis));
+ }
+ }
+
+ // Storage engine transactions may be started in a lazy manner. By explicitly
+ // starting here we ensure that a point-in-time snapshot is established during the
+ // first operation of a transaction.
+ //
+ // Active transactions are protected by the locking subsystem, so we must always hold at least a
+ // Global intent lock before starting a transaction. We pessimistically acquire an intent
+ // exclusive lock here because we might be doing writes in this transaction, and it is currently
+ // not deadlock-safe to upgrade IS to IX.
+ Lock::GlobalLock(opCtx, MODE_IX);
+ opCtx->recoveryUnit()->preallocateSnapshot();
+
+ // The Client lock must not be held when executing this failpoint as it will block currentOp
+ // execution.
+ MONGO_FAIL_POINT_PAUSE_WHILE_SET(hangAfterPreallocateSnapshot);
+}
+
+Timestamp TransactionParticipant::prepareTransaction(OperationContext* opCtx) {
+ // This ScopeGuard is created outside of the lock so that the lock is always released before
+ // this is called.
+ ScopeGuard abortGuard = MakeGuard([&] { abortActiveTransaction(opCtx); });
+
+ stdx::unique_lock<stdx::mutex> lk(_mutex);
+ // Always check session's txnNumber and '_txnState', since they can be modified by
+ // session kill and migration, which do not check out the session.
+ _checkIsActiveTransaction(lk, *opCtx->getTxnNumber(), true);
+
+ uassert(ErrorCodes::TransactionCommitted,
+ str::stream() << "Transaction " << *opCtx->getTxnNumber() << " has been committed.",
+ !_txnState.isCommitted(lk));
+
+ _getSession()->lockTxnNumber(
+ _activeTxnNumber,
+ {ErrorCodes::PreparedTransactionInProgress,
+ "cannot change transaction number while the session has a prepared transaction"});
+ _txnState.transitionTo(lk, TransactionState::kPrepared);
+
+ // Reserve an optime for the 'prepareTimestamp'. This will create a hole in the oplog and cause
+ // 'snapshot' and 'afterClusterTime' readers to block until this transaction is done being
+ // prepared. When the OplogSlotReserver goes out of scope and is destroyed, the
+ // storage-transaction it uses to keep the hole open will abort and the slot (and corresponding
+ // oplog hole) will vanish.
+ OplogSlotReserver oplogSlotReserver(opCtx);
+ const auto prepareOplogSlot = oplogSlotReserver.getReservedOplogSlot();
+ const auto prepareTimestamp = prepareOplogSlot.opTime.getTimestamp();
+
+ if (MONGO_FAIL_POINT(hangAfterReservingPrepareTimestamp)) {
+ // This log output is used in js tests so please leave it.
+ log() << "transaction - hangAfterReservingPrepareTimestamp fail point "
+ "enabled. Blocking until fail point is disabled. Prepare OpTime: "
+ << prepareOplogSlot.opTime;
+ MONGO_FAIL_POINT_PAUSE_WHILE_SET(hangAfterReservingPrepareTimestamp);
+ }
+
+ opCtx->recoveryUnit()->setPrepareTimestamp(prepareTimestamp);
+ opCtx->getWriteUnitOfWork()->prepare();
+
+ // We need to unlock the session to run the opObserver onTransactionPrepare, which calls back
+ // into the session.
+ lk.unlock();
+ auto opObserver = opCtx->getServiceContext()->getOpObserver();
+ invariant(opObserver);
+ opObserver->onTransactionPrepare(opCtx, prepareOplogSlot);
+
+ // After the oplog entry is written successfully, it is illegal to implicitly abort or fail.
+ try {
+ abortGuard.Dismiss();
+
+ lk.lock();
+
+ // Although we are not allowed to abort here, we check that we don't even try to. If we do
+ // try to, that is a bug and we will fassert below.
+ _checkIsActiveTransaction(lk, *opCtx->getTxnNumber(), true);
+
+ // Ensure that the transaction is still prepared.
+ invariant(_txnState.isPrepared(lk), str::stream() << "Current state: " << _txnState);
+ } catch (...) {
+ severe() << "Illegal exception after transaction was prepared.";
+ fassertFailedWithStatus(50906, exceptionToStatus());
+ }
+
+ return prepareTimestamp;
+}
+
+void TransactionParticipant::addTransactionOperation(OperationContext* opCtx,
+ const repl::ReplOperation& operation) {
+ stdx::lock_guard<stdx::mutex> lk(_mutex);
+
+ // Always check _getSession()'s txnNumber and '_txnState', since they can be modified by session
+ // kill and migration, which do not check out the session.
+ _checkIsActiveTransaction(lk, *opCtx->getTxnNumber(), true);
+
+ // Ensure that we only ever add operations to an in progress transaction.
+ invariant(_txnState.isInProgress(lk), str::stream() << "Current state: " << _txnState);
+
+ invariant(_autoCommit && !*_autoCommit && _activeTxnNumber != kUninitializedTxnNumber);
+ invariant(opCtx->lockState()->inAWriteUnitOfWork());
+ _transactionOperations.push_back(operation);
+ _transactionOperationBytes += repl::OplogEntry::getReplOperationSize(operation);
+ // _transactionOperationBytes is based on the in-memory size of the operation. With overhead,
+ // we expect the BSON size of the operation to be larger, so it's possible to make a transaction
+ // just a bit too large and have it fail only in the commit. It's still useful to fail early
+ // when possible (e.g. to avoid exhausting server memory).
+ uassert(ErrorCodes::TransactionTooLarge,
+ str::stream() << "Total size of all transaction operations must be less than "
+ << BSONObjMaxInternalSize
+ << ". Actual size is "
+ << _transactionOperationBytes,
+ _transactionOperationBytes <= BSONObjMaxInternalSize);
+}
+
+std::vector<repl::ReplOperation> TransactionParticipant::endTransactionAndRetrieveOperations(
+ OperationContext* opCtx) {
+ stdx::lock_guard<stdx::mutex> lk(_mutex);
+
+ // Always check session's txnNumber and '_txnState', since they can be modified by session kill
+ // and migration, which do not check out the session.
+ _checkIsActiveTransaction(lk, *opCtx->getTxnNumber(), true);
+
+ // Ensure that we only ever end a transaction when prepared or committing.
+ invariant(_txnState.isPrepared(lk) || _txnState.isCommittingWithoutPrepare(lk),
+ str::stream() << "Current state: " << _txnState);
+
+ invariant(_autoCommit);
+ _transactionOperationBytes = 0;
+ return std::move(_transactionOperations);
+}
+
+void TransactionParticipant::commitUnpreparedTransaction(OperationContext* opCtx) {
+ stdx::unique_lock<stdx::mutex> lk(_mutex);
+
+ uassert(ErrorCodes::InvalidOptions,
+ "commitTransaction must provide commitTimestamp to prepared transaction.",
+ !_txnState.isPrepared(lk));
+
+ // Always check session's txnNumber and '_txnState', since they can be modified by session kill
+ // and migration, which do not check out the session.
+ _checkIsActiveTransaction(lk, *opCtx->getTxnNumber(), true);
+
+ _txnState.transitionTo(lk, TransactionState::kCommittingWithoutPrepare);
+
+ // We need to unlock the session to run the opObserver onTransactionCommit, which calls back
+ // into the session.
+ lk.unlock();
+
+ auto opObserver = opCtx->getServiceContext()->getOpObserver();
+ invariant(opObserver);
+ opObserver->onTransactionCommit(opCtx, false /* wasPrepared */);
+
+ lk.lock();
+
+ _checkIsActiveTransaction(lk, *opCtx->getTxnNumber(), true);
+ _commitTransaction(std::move(lk), opCtx);
+}
+
+void TransactionParticipant::commitPreparedTransaction(OperationContext* opCtx,
+ Timestamp commitTimestamp) {
+ stdx::unique_lock<stdx::mutex> lk(_mutex);
+ uassert(ErrorCodes::InvalidOptions,
+ "commitTransaction cannot provide commitTimestamp to unprepared transaction.",
+ _txnState.isPrepared(lk));
+ uassert(
+ ErrorCodes::InvalidOptions, "'commitTimestamp' cannot be null", !commitTimestamp.isNull());
+
+ // Always check '_activeTxnNumber' and '_txnState', since they can be modified by session kill
+ // and migration, which do not check out the session.
+ _checkIsActiveTransaction(lk, *opCtx->getTxnNumber(), true);
+ _txnState.transitionTo(lk, TransactionState::kCommittingWithPrepare);
+ opCtx->recoveryUnit()->setCommitTimestamp(commitTimestamp);
+
+ // We need to unlock the session to run the opObserver onTransactionCommit, which calls back
+ // into the session.
+ lk.unlock();
+
+ auto opObserver = opCtx->getServiceContext()->getOpObserver();
+ invariant(opObserver);
+ opObserver->onTransactionCommit(opCtx, true /* wasPrepared */);
+
+ lk.lock();
+
+ _checkIsActiveTransaction(lk, *opCtx->getTxnNumber(), true);
+
+ _commitTransaction(std::move(lk), opCtx);
+ _getSession()->unlockTxnNumber();
+}
+
+void TransactionParticipant::_commitTransaction(stdx::unique_lock<stdx::mutex> lk,
+ OperationContext* opCtx) {
+ auto abortGuard = MakeGuard([this, opCtx]() {
+ _abortActiveTransaction(opCtx,
+ TransactionState::kCommittingWithoutPrepare |
+ TransactionState::kCommittingWithPrepare);
+ });
+
+ lk.unlock();
+
+ opCtx->getWriteUnitOfWork()->commit();
+ opCtx->setWriteUnitOfWork(nullptr);
+ abortGuard.Dismiss();
+
+ lk.lock();
+
+ auto& clientInfo = repl::ReplClientInfo::forClient(opCtx->getClient());
+
+ // If no writes have been done, set the client optime forward to the read timestamp so waiting
+ // for write concern will ensure all read data was committed.
+ //
+ // TODO(SERVER-34881): Once the default read concern is speculative majority, only set the
+ // client optime forward if the original read concern level is "majority" or "snapshot".
+ if (_speculativeTransactionReadOpTime > clientInfo.getLastOp()) {
+ clientInfo.setLastOp(_speculativeTransactionReadOpTime);
+ }
+
+ _txnState.transitionTo(lk, TransactionState::kCommitted);
+
+ ServerTransactionsMetrics::get(opCtx)->incrementTotalCommitted();
+
+ // After the transaction has been committed, we must update the end time and mark it as
+ // inactive.
+ _singleTransactionStats.setEndTime(curTimeMicros64());
+ if (_singleTransactionStats.isActive()) {
+ _singleTransactionStats.setInactive(curTimeMicros64());
+ }
+
+ ServerTransactionsMetrics::get(opCtx)->decrementCurrentOpen();
+ ServerTransactionsMetrics::get(getGlobalServiceContext())->decrementCurrentActive();
+
+ // Add the latest operation stats to the aggregate OpDebug object stored in the
+ // SingleTransactionStats instance on the Session.
+ _singleTransactionStats.getOpDebug()->additiveMetrics.add(
+ CurOp::get(opCtx)->debug().additiveMetrics);
+
+ // Update the LastClientInfo object stored in the SingleTransactionStats instance on the Session
+ // with this Client's information.
+ _singleTransactionStats.updateLastClientInfo(opCtx->getClient());
+
+ // Log the transaction if its duration is longer than the slowMS command threshold.
+ _logSlowTransaction(lk,
+ &(opCtx->lockState()->getLockerInfo())->stats,
+ TransactionState::kCommitted,
+ repl::ReadConcernArgs::get(opCtx));
+
+ // We must clear the recovery unit and locker so any post-transaction writes can run without
+ // transactional settings such as a read timestamp.
+ _cleanUpTxnResourceOnOpCtx(lk, opCtx);
}
-void TransactionParticipant::create(Session* session) {
- invariant(!getTransactionParticipant(session));
- getTransactionParticipant(session).emplace();
+void TransactionParticipant::abortArbitraryTransaction() {
+ stdx::lock_guard<stdx::mutex> lock(_mutex);
+ _abortArbitraryTransaction(lock);
+}
+
+void TransactionParticipant::abortArbitraryTransactionIfExpired() {
+ stdx::lock_guard<stdx::mutex> lock(_mutex);
+ if (!_transactionExpireDate || _transactionExpireDate >= Date_t::now()) {
+ return;
+ }
+
+ _abortArbitraryTransaction(lock);
+}
+
+void TransactionParticipant::_abortArbitraryTransaction(WithLock lock) {
+ if (!_txnState.isInProgress(lock)) {
+ // We do not want to abort transactions that are prepared unless we get an
+ // 'abortTransaction' command.
+ return;
+ }
+
+ _abortTransactionOnSession(lock);
+}
+
+void TransactionParticipant::abortActiveTransaction(OperationContext* opCtx) {
+ _abortActiveTransaction(opCtx, TransactionState::kInProgress | TransactionState::kPrepared);
+}
+
+void TransactionParticipant::_abortActiveTransaction(OperationContext* opCtx,
+ TransactionState::StateSet expectedStates) {
+ stdx::lock_guard<stdx::mutex> lock(_mutex);
+
+ invariant(!_txnResourceStash);
+
+ if (!_txnState.isNone(lock)) {
+ // Add the latest operation stats to the aggregate OpDebug object stored in the
+ // SingleTransactionStats instance on the Session.
+ _singleTransactionStats.getOpDebug()->additiveMetrics.add(
+ CurOp::get(opCtx)->debug().additiveMetrics);
+
+ // Update the LastClientInfo object stored in the SingleTransactionStats instance on the
+ // Session with this Client's information.
+ _singleTransactionStats.updateLastClientInfo(opCtx->getClient());
+ }
+
+ // Only abort the transaction in session if it's in expected states.
+ // When the state of active transaction on session is not expected, it means another
+ // thread has already aborted the transaction on session.
+ if (_txnState.isInSet(lock, expectedStates)) {
+ invariant(opCtx->getTxnNumber() == _activeTxnNumber);
+ _abortTransactionOnSession(lock);
+ }
+
+ // Log the transaction if its duration is longer than the slowMS command threshold.
+ _logSlowTransaction(lock,
+ &(opCtx->lockState()->getLockerInfo())->stats,
+ TransactionState::kAborted,
+ repl::ReadConcernArgs::get(opCtx));
+
+ // Clean up the transaction resources on opCtx even if the transaction on session has been
+ // aborted.
+ _cleanUpTxnResourceOnOpCtx(lock, opCtx);
+}
+
+void TransactionParticipant::_abortTransactionOnSession(WithLock wl) {
+ if (!_txnState.isNone(wl)) {
+ _singleTransactionStats.setEndTime(curTimeMicros64());
+ // The transaction has aborted, so we mark it as inactive.
+ if (_singleTransactionStats.isActive()) {
+ _singleTransactionStats.setInactive(curTimeMicros64());
+ }
+ }
+
+ // If the transaction is stashed, then we have aborted an inactive transaction.
+ if (_txnResourceStash) {
+ // The transaction is stashed, so we abort the inactive transaction on session.
+ _logSlowTransaction(wl,
+ &(_txnResourceStash->locker()->getLockerInfo())->stats,
+ TransactionState::kAborted,
+ _txnResourceStash->getReadConcernArgs());
+ _txnResourceStash = boost::none;
+ ServerTransactionsMetrics::get(getGlobalServiceContext())->decrementCurrentInactive();
+ } else {
+ // Transaction resource has been unstashed and transferred into an active opCtx, which will
+ // clean it up.
+ ServerTransactionsMetrics::get(getGlobalServiceContext())->decrementCurrentActive();
+ }
+
+ _transactionOperationBytes = 0;
+ _transactionOperations.clear();
+ _txnState.transitionTo(wl, TransactionState::kAborted);
+ _speculativeTransactionReadOpTime = repl::OpTime();
+
+ _getSession()->unlockTxnNumber();
+
+ ServerTransactionsMetrics::get(getGlobalServiceContext())->incrementTotalAborted();
+ ServerTransactionsMetrics::get(getGlobalServiceContext())->decrementCurrentOpen();
+}
+
+void TransactionParticipant::_cleanUpTxnResourceOnOpCtx(WithLock wl, OperationContext* opCtx) {
+ // Reset the WUOW. We should be able to abort empty transactions that don't have WUOW.
+ if (opCtx->getWriteUnitOfWork()) {
+ opCtx->setWriteUnitOfWork(nullptr);
+ }
+
+ // We must clear the recovery unit and locker so any post-transaction writes can run without
+ // transactional settings such as a read timestamp.
+ opCtx->setRecoveryUnit(opCtx->getServiceContext()->getStorageEngine()->newRecoveryUnit(),
+ WriteUnitOfWork::RecoveryUnitState::kNotInUnitOfWork);
+
+ opCtx->lockState()->unsetMaxLockTimeout();
+}
+
+void TransactionParticipant::_checkIsActiveTransaction(WithLock wl,
+ const TxnNumber& requestTxnNumber,
+ bool checkAbort) const {
+ const auto txnNumber = _getSession()->getActiveTxnNumber();
+ uassert(ErrorCodes::ConflictingOperationInProgress,
+ str::stream() << "Cannot perform operations on transaction " << _activeTxnNumber
+ << " on session "
+ << _getSession()->getSessionId()
+ << " because a different transaction "
+ << txnNumber
+ << " is now active.",
+ txnNumber == _activeTxnNumber);
+
+ uassert(ErrorCodes::ConflictingOperationInProgress,
+ str::stream() << "Cannot perform operations on transaction " << requestTxnNumber
+ << " on session "
+ << _getSession()->getSessionId()
+ << " because a different transaction "
+ << _activeTxnNumber
+ << " is now active.",
+ requestTxnNumber == _activeTxnNumber);
+
+ uassert(ErrorCodes::NoSuchTransaction,
+ str::stream() << "Transaction " << txnNumber << " has been aborted.",
+ !checkAbort || !_txnState.isAborted(wl));
+}
+
+Status TransactionParticipant::isValid(StringData dbName, StringData cmdName) {
+ if (cmdName == "count"_sd) {
+ return {ErrorCodes::OperationNotSupportedInTransaction,
+ "Cannot run 'count' in a multi-document transaction. Please see "
+ "http://dochub.mongodb.org/core/transaction-count for a recommended alternative."};
+ }
+
+ if (txnCmdWhitelist.find(cmdName) == txnCmdWhitelist.cend() &&
+ !(getTestCommandsEnabled() &&
+ txnCmdForTestingWhitelist.find(cmdName) != txnCmdForTestingWhitelist.cend())) {
+ return {ErrorCodes::OperationNotSupportedInTransaction,
+ str::stream() << "Cannot run '" << cmdName << "' in a multi-document transaction."};
+ }
+
+ if (dbName == "config"_sd || dbName == "local"_sd ||
+ (dbName == "admin"_sd && txnAdminCommands.find(cmdName) == txnAdminCommands.cend())) {
+ return {ErrorCodes::OperationNotSupportedInTransaction,
+ str::stream() << "Cannot run command against the '" << dbName
+ << "' database in a transaction"};
+ }
+
+ return Status::OK();
+}
+
+BSONObj TransactionParticipant::reportStashedState() const {
+ BSONObjBuilder builder;
+ reportStashedState(&builder);
+ return builder.obj();
+}
+
+void TransactionParticipant::reportStashedState(BSONObjBuilder* builder) const {
+ stdx::lock_guard<stdx::mutex> ls(_mutex);
+
+ if (_txnResourceStash && _txnResourceStash->locker()) {
+ if (auto lockerInfo = _txnResourceStash->locker()->getLockerInfo()) {
+ invariant(_activeTxnNumber != kUninitializedTxnNumber);
+ builder->append("host", getHostNameCachedAndPort());
+ builder->append("desc", "inactive transaction");
+
+ auto lastClientInfo = _singleTransactionStats.getLastClientInfo();
+ builder->append("client", lastClientInfo.clientHostAndPort);
+ builder->append("connectionId", lastClientInfo.connectionId);
+ builder->append("appName", lastClientInfo.appName);
+ builder->append("clientMetadata", lastClientInfo.clientMetadata);
+
+ {
+ BSONObjBuilder lsid(builder->subobjStart("lsid"));
+ _getSession()->getSessionId().serialize(&lsid);
+ }
+
+ BSONObjBuilder transactionBuilder;
+ _reportTransactionStats(
+ ls, &transactionBuilder, _txnResourceStash->getReadConcernArgs());
+
+ builder->append("transaction", transactionBuilder.obj());
+ builder->append("waitingForLock", false);
+ builder->append("active", false);
+
+ fillLockerInfo(*lockerInfo, *builder);
+ }
+ }
+}
+
+void TransactionParticipant::reportUnstashedState(repl::ReadConcernArgs readConcernArgs,
+ BSONObjBuilder* builder) const {
+ stdx::lock_guard<stdx::mutex> ls(_mutex);
+
+ if (!_txnResourceStash) {
+ BSONObjBuilder transactionBuilder;
+ _reportTransactionStats(ls, &transactionBuilder, readConcernArgs);
+ builder->append("transaction", transactionBuilder.obj());
+ }
+}
+
+std::string TransactionParticipant::TransactionState::toString(StateFlag state) {
+ switch (state) {
+ case TransactionParticipant::TransactionState::kNone:
+ return "TxnState::None";
+ case TransactionParticipant::TransactionState::kInProgress:
+ return "TxnState::InProgress";
+ case TransactionParticipant::TransactionState::kPrepared:
+ return "TxnState::Prepared";
+ case TransactionParticipant::TransactionState::kCommittingWithoutPrepare:
+ return "TxnState::CommittingWithoutPrepare";
+ case TransactionParticipant::TransactionState::kCommittingWithPrepare:
+ return "TxnState::CommittingWithPrepare";
+ case TransactionParticipant::TransactionState::kCommitted:
+ return "TxnState::Committed";
+ case TransactionParticipant::TransactionState::kAborted:
+ return "TxnState::Aborted";
+ }
+ MONGO_UNREACHABLE;
+}
+
+bool TransactionParticipant::TransactionState::_isLegalTransition(StateFlag oldState,
+ StateFlag newState) {
+ switch (oldState) {
+ case kNone:
+ switch (newState) {
+ case kNone:
+ case kInProgress:
+ return true;
+ default:
+ return false;
+ }
+ MONGO_UNREACHABLE;
+ case kInProgress:
+ switch (newState) {
+ case kNone:
+ case kPrepared:
+ case kCommittingWithoutPrepare:
+ case kAborted:
+ return true;
+ default:
+ return false;
+ }
+ MONGO_UNREACHABLE;
+ case kPrepared:
+ switch (newState) {
+ case kCommittingWithPrepare:
+ case kAborted:
+ return true;
+ default:
+ return false;
+ }
+ MONGO_UNREACHABLE;
+ case kCommittingWithPrepare:
+ case kCommittingWithoutPrepare:
+ switch (newState) {
+ case kNone:
+ case kCommitted:
+ case kAborted:
+ return true;
+ default:
+ return false;
+ }
+ MONGO_UNREACHABLE;
+ case kCommitted:
+ switch (newState) {
+ case kNone:
+ case kInProgress:
+ return true;
+ default:
+ return false;
+ }
+ MONGO_UNREACHABLE;
+ case kAborted:
+ switch (newState) {
+ case kNone:
+ case kInProgress:
+ return true;
+ default:
+ return false;
+ }
+ MONGO_UNREACHABLE;
+ }
+ MONGO_UNREACHABLE;
+}
+
+void TransactionParticipant::TransactionState::transitionTo(WithLock,
+ StateFlag newState,
+ TransitionValidation shouldValidate) {
+ if (shouldValidate == TransitionValidation::kValidateTransition) {
+ invariant(TransactionState::_isLegalTransition(_state, newState),
+ str::stream() << "Current state: " << toString(_state)
+ << ", Illegal attempted next state: "
+ << toString(newState));
+ }
+
+ _state = newState;
+}
+
+void TransactionParticipant::_reportTransactionStats(WithLock wl,
+ BSONObjBuilder* builder,
+ repl::ReadConcernArgs readConcernArgs) const {
+ BSONObjBuilder parametersBuilder(builder->subobjStart("parameters"));
+ parametersBuilder.append("txnNumber", _activeTxnNumber);
+
+ if (!_txnState.inMultiDocumentTransaction(wl)) {
+ // For retryable writes, we only include the txnNumber.
+ parametersBuilder.done();
+ return;
+ }
+
+ parametersBuilder.append("autocommit", _autoCommit ? *_autoCommit : true);
+ readConcernArgs.appendInfo(&parametersBuilder);
+ parametersBuilder.done();
+
+ builder->append("readTimestamp", _speculativeTransactionReadOpTime.getTimestamp());
+ builder->append("startWallClockTime",
+ dateToISOStringLocal(Date_t::fromMillisSinceEpoch(
+ _singleTransactionStats.getStartTime() / 1000)));
+
+ // We use the same "now" time so that the following time metrics are consistent with each other.
+ auto curTime = curTimeMicros64();
+ builder->append("timeOpenMicros",
+ static_cast<long long>(_singleTransactionStats.getDuration(curTime)));
+
+ auto timeActive =
+ durationCount<Microseconds>(_singleTransactionStats.getTimeActiveMicros(curTime));
+ auto timeInactive =
+ durationCount<Microseconds>(_singleTransactionStats.getTimeInactiveMicros(curTime));
+
+ builder->append("timeActiveMicros", timeActive);
+ builder->append("timeInactiveMicros", timeInactive);
+
+ if (_transactionExpireDate) {
+ builder->append("expiryTime", dateToISOStringLocal(*_transactionExpireDate));
+ }
+}
+
+void TransactionParticipant::_updateState(WithLock wl, const Session::RefreshState& newState) {
+ if (newState.refreshCount <= _lastStateRefreshCount) {
+ return;
+ }
+
+ _activeTxnNumber = newState.txnNumber;
+ if (newState.isCommitted) {
+ _txnState.transitionTo(wl,
+ TransactionState::kCommitted,
+ TransactionState::TransitionValidation::kRelaxTransitionValidation);
+ }
+
+ _lastStateRefreshCount = newState.refreshCount;
}
//
@@ -120,4 +1157,98 @@ Action TransactionParticipant::StateMachine::onEvent(Event event) {
return transition.action;
}
+std::string TransactionParticipant::_transactionInfoForLog(
+ const SingleThreadedLockStats* lockStats,
+ TransactionState::StateFlag terminationCause,
+ repl::ReadConcernArgs readConcernArgs) {
+ invariant(lockStats);
+ invariant(terminationCause == TransactionState::kCommitted ||
+ terminationCause == TransactionState::kAborted);
+
+ StringBuilder s;
+
+ // User specified transaction parameters.
+ BSONObjBuilder parametersBuilder;
+
+ BSONObjBuilder lsidBuilder(parametersBuilder.subobjStart("lsid"));
+ _getSession()->getSessionId().serialize(&lsidBuilder);
+ lsidBuilder.doneFast();
+
+ parametersBuilder.append("txnNumber", _activeTxnNumber);
+ parametersBuilder.append("autocommit", _autoCommit ? *_autoCommit : true);
+ readConcernArgs.appendInfo(&parametersBuilder);
+
+ s << "parameters:" << parametersBuilder.obj().toString() << ",";
+
+ s << " readTimestamp:" << _speculativeTransactionReadOpTime.getTimestamp().toString() << ",";
+
+ s << _singleTransactionStats.getOpDebug()->additiveMetrics.report();
+
+ std::string terminationCauseString =
+ terminationCause == TransactionState::kCommitted ? "committed" : "aborted";
+ s << " terminationCause:" << terminationCauseString;
+
+ auto curTime = curTimeMicros64();
+ s << " timeActiveMicros:"
+ << durationCount<Microseconds>(_singleTransactionStats.getTimeActiveMicros(curTime));
+ s << " timeInactiveMicros:"
+ << durationCount<Microseconds>(_singleTransactionStats.getTimeInactiveMicros(curTime));
+
+ // Number of yields is always 0 in multi-document transactions, but it is included mainly to
+ // match the format with other slow operation logging messages.
+ s << " numYields:" << 0;
+ // Aggregate lock statistics.
+
+ BSONObjBuilder locks;
+ lockStats->report(&locks);
+ s << " locks:" << locks.obj().toString();
+
+ // Total duration of the transaction.
+ s << " "
+ << Milliseconds{static_cast<long long>(_singleTransactionStats.getDuration(curTime)) / 1000};
+
+ return s.str();
+}
+
+void TransactionParticipant::_logSlowTransaction(WithLock wl,
+ const SingleThreadedLockStats* lockStats,
+ TransactionState::StateFlag terminationCause,
+ repl::ReadConcernArgs readConcernArgs) {
+ // Only log multi-document transactions.
+ if (!_txnState.isNone(wl)) {
+ // Log the transaction if its duration is longer than the slowMS command threshold.
+ if (_singleTransactionStats.getDuration(curTimeMicros64()) >
+ serverGlobalParams.slowMS * 1000ULL) {
+ log(logger::LogComponent::kTransaction)
+ << "transaction "
+ << _transactionInfoForLog(lockStats, terminationCause, readConcernArgs);
+ }
+ }
+}
+
+void TransactionParticipant::checkForNewTxnNumber() {
+ auto txnNumber = _getSession()->getActiveTxnNumber();
+
+ stdx::lock_guard<stdx::mutex> lg(_mutex);
+ if (txnNumber > _activeTxnNumber) {
+ _setNewTxnNumber(lg, txnNumber);
+ }
+}
+
+void TransactionParticipant::_setNewTxnNumber(WithLock wl, const TxnNumber& txnNumber) {
+ invariant(!_txnState.isPrepared(wl));
+
+ // Abort the existing transaction if it's not prepared, committed, or aborted.
+ if (_txnState.isInProgress(wl)) {
+ _abortTransactionOnSession(wl);
+ }
+
+ _activeTxnNumber = txnNumber;
+ _txnState.transitionTo(wl, TransactionState::kNone);
+ _singleTransactionStats = SingleTransactionStats();
+ _speculativeTransactionReadOpTime = repl::OpTime();
+ _multikeyPathInfo.clear();
+ _autoCommit = boost::none;
+}
+
} // namespace mongo
diff --git a/src/mongo/db/transaction_participant.h b/src/mongo/db/transaction_participant.h
index bcb13bef0b1..d17d222410c 100644
--- a/src/mongo/db/transaction_participant.h
+++ b/src/mongo/db/transaction_participant.h
@@ -33,13 +33,24 @@
#include <map>
#include "mongo/base/disallow_copying.h"
+#include "mongo/db/concurrency/locker.h"
+#include "mongo/db/logical_session_id.h"
+#include "mongo/db/multi_key_path_tracker.h"
+#include "mongo/db/repl/oplog.h"
+#include "mongo/db/repl/read_concern_args.h"
+#include "mongo/db/session.h"
+#include "mongo/db/single_transaction_stats.h"
+#include "mongo/db/storage/recovery_unit.h"
#include "mongo/util/assert_util.h"
+#include "mongo/util/concurrency/with_lock.h"
#include "mongo/util/decorable.h"
#include "mongo/util/mongoutils/str.h"
namespace mongo {
-class Session;
+class OperationContext;
+
+extern AtomicInt32 transactionLifetimeLimitSeconds;
/**
* A state machine that coordinates a distributed transaction commit with the transaction
@@ -49,8 +60,78 @@ class TransactionParticipant {
MONGO_DISALLOW_COPYING(TransactionParticipant);
public:
+ /**
+ * Holds state for a snapshot read or multi-statement transaction in between network
+ * operations.
+ */
+ class TxnResources {
+ public:
+ /**
+ * Stashes transaction state from 'opCtx' in the newly constructed TxnResources.
+ */
+ TxnResources(OperationContext* opCtx, bool keepTicket = false);
+
+ ~TxnResources();
+
+ // Rule of 5: because we have a class-defined destructor, we need to explictly specify
+ // the move operator and move assignment operator.
+ TxnResources(TxnResources&&) = default;
+ TxnResources& operator=(TxnResources&&) = default;
+
+ /**
+ * Returns a const pointer to the stashed lock state, or nullptr if no stashed locks exist.
+ */
+ const Locker* locker() const {
+ return _locker.get();
+ }
+
+ /**
+ * Releases stashed transaction state onto 'opCtx'. Must only be called once.
+ */
+ void release(OperationContext* opCtx);
+
+ /**
+ * Returns the read concern arguments.
+ */
+ repl::ReadConcernArgs getReadConcernArgs() const {
+ return _readConcernArgs;
+ }
+
+ private:
+ bool _released = false;
+ std::unique_ptr<Locker> _locker;
+ std::unique_ptr<RecoveryUnit> _recoveryUnit;
+ repl::ReadConcernArgs _readConcernArgs;
+ WriteUnitOfWork::RecoveryUnitState _ruState;
+ };
+
+ /**
+ * An RAII object that stashes `TxnResouces` from the `opCtx` onto the stack. At destruction
+ * it unstashes the `TxnResources` back onto the `opCtx`.
+ */
+ class SideTransactionBlock {
+ public:
+ SideTransactionBlock(OperationContext* opCtx);
+ ~SideTransactionBlock();
+
+ // Rule of 5: because we have a class-defined destructor, we need to explictly specify
+ // the move operator and move assignment operator.
+ SideTransactionBlock(SideTransactionBlock&&) = default;
+ SideTransactionBlock& operator=(SideTransactionBlock&&) = default;
+
+ private:
+ boost::optional<TxnResources> _txnResources;
+ OperationContext* _opCtx;
+ };
+
+ static TransactionParticipant* get(OperationContext* opCtx);
+
+ /**
+ * This should only be used when session was obtained without checking it out.
+ */
+ static TransactionParticipant* getFromNonCheckedOutSession(Session* session);
+
TransactionParticipant() = default;
- ~TransactionParticipant() = default;
static boost::optional<TransactionParticipant>& get(Session* session);
static void create(Session* session);
@@ -115,8 +196,413 @@ public:
State _state{State::kUnprepared};
};
+ /**
+ * Called for speculative transactions to fix the optime of the snapshot to read from.
+ */
+ void setSpeculativeTransactionOpTimeToLastApplied(OperationContext* opCtx);
+
+ /**
+ * Transfers management of transaction resources from the OperationContext to the Session.
+ */
+ void stashTransactionResources(OperationContext* opCtx);
+
+ /**
+ * Transfers management of transaction resources from the Session to the OperationContext.
+ */
+ void unstashTransactionResources(OperationContext* opCtx, const std::string& cmdName);
+
+ /**
+ * Commits the transaction, including committing the write unit of work and updating
+ * transaction state.
+ *
+ * Throws an exception if the transaction is prepared.
+ */
+ void commitUnpreparedTransaction(OperationContext* opCtx);
+
+ /**
+ * Commits the transaction, including committing the write unit of work and updating
+ * transaction state.
+ *
+ * Throws an exception if the transaction is not prepared or if the 'commitTimestamp' is null.
+ */
+ void commitPreparedTransaction(OperationContext* opCtx, Timestamp commitTimestamp);
+
+ /**
+ * Puts a transaction into a prepared state and returns the prepareTimestamp.
+ */
+ Timestamp prepareTransaction(OperationContext* opCtx);
+
+ /**
+ * Returns whether we are in a multi-document transaction, which means we have an active
+ * transaction which has autoCommit:false and has not been committed or aborted. It is possible
+ * that the current transaction is stashed onto the stack via a `SideTransactionBlock`.
+ */
+ bool inMultiDocumentTransaction() const {
+ stdx::lock_guard<stdx::mutex> lk(_mutex);
+ return _txnState.inMultiDocumentTransaction(lk);
+ };
+
+ bool transactionIsCommitted() const {
+ stdx::lock_guard<stdx::mutex> lk(_mutex);
+ return _txnState.isCommitted(lk);
+ }
+
+ bool transactionIsAborted() const {
+ stdx::lock_guard<stdx::mutex> lk(_mutex);
+ return _txnState.isAborted(lk);
+ }
+
+ /**
+ * Adds a stored operation to the list of stored operations for the current multi-document
+ * (non-autocommit) transaction. It is illegal to add operations when no multi-document
+ * transaction is in progress.
+ */
+ void addTransactionOperation(OperationContext* opCtx, const repl::ReplOperation& operation);
+
+ /**
+ * Returns and clears the stored operations for an multi-document (non-autocommit) transaction,
+ * and marks the transaction as closed. It is illegal to attempt to add operations to the
+ * transaction after this is called.
+ */
+ std::vector<repl::ReplOperation> endTransactionAndRetrieveOperations(OperationContext* opCtx);
+
+ const std::vector<repl::ReplOperation>& transactionOperationsForTest() {
+ return _transactionOperations;
+ }
+
+ SingleTransactionStats getSingleTransactionStats() const {
+ return _singleTransactionStats;
+ }
+
+ repl::OpTime getSpeculativeTransactionReadOpTimeForTest() const {
+ stdx::lock_guard<stdx::mutex> lk(_mutex);
+ return _speculativeTransactionReadOpTime;
+ }
+
+ const Locker* getTxnResourceStashLockerForTest() const {
+ stdx::lock_guard<stdx::mutex> lk(_mutex);
+ invariant(_txnResourceStash);
+ return _txnResourceStash->locker();
+ }
+
+ /**
+ * If this session is holding stashed locks in _txnResourceStash, reports the current state of
+ * the session using the provided builder. Locks the session object's mutex while running.
+ */
+ void reportStashedState(BSONObjBuilder* builder) const;
+
+ std::string transactionInfoForLogForTest(const SingleThreadedLockStats* lockStats,
+ bool committed,
+ repl::ReadConcernArgs readConcernArgs) {
+ stdx::lock_guard<stdx::mutex> lk(_mutex);
+ TransactionState::StateFlag terminationCause =
+ committed ? TransactionState::kCommitted : TransactionState::kAborted;
+ return _transactionInfoForLog(lockStats, terminationCause, readConcernArgs);
+ }
+
+ /**
+ * If this session is not holding stashed locks in _txnResourceStash (transaction is active),
+ * reports the current state of the session using the provided builder. Locks the session
+ * object's mutex while running.
+ */
+ void reportUnstashedState(repl::ReadConcernArgs readConcernArgs, BSONObjBuilder* builder) const;
+
+ /**
+ * Convenience method which creates and populates a BSONObj containing the stashed state.
+ * Returns an empty BSONObj if this session has no stashed resources.
+ */
+ BSONObj reportStashedState() const;
+
+ /**
+ * Aborts the transaction outside the transaction, releasing transaction resources.
+ *
+ * Not called with session checked out.
+ */
+ void abortArbitraryTransaction();
+
+ /**
+ * Same as abortArbitraryTransaction, except only executes if _transactionExpireDate indicates
+ * that the transaction has expired.
+ *
+ * Not called with session checked out.
+ */
+ void abortArbitraryTransactionIfExpired();
+
+ /*
+ * Aborts the transaction inside the transaction, releasing transaction resources.
+ * We're inside the transaction when we have the Session checked out and 'opCtx' owns the
+ * transaction resources.
+ */
+ void abortActiveTransaction(OperationContext* opCtx);
+
+ void addMultikeyPathInfo(MultikeyPathInfo info) {
+ _multikeyPathInfo.push_back(std::move(info));
+ }
+
+ const std::vector<MultikeyPathInfo>& getMultikeyPathInfo() const {
+ return _multikeyPathInfo;
+ }
+
+ /**
+ * Starts a new transaction, or continues an already active transaction.
+ *
+ * The 'autocommit' argument represents the value of the field given in the original client
+ * request. If it is boost::none, no autocommit parameter was passed into the request. Every
+ * operation that is part of a multi statement transaction must specify 'autocommit=false'.
+ * 'startTransaction' represents the value of the field given in the original client request,
+ * and indicates whether this operation is the beginning of a multi-statement transaction.
+ *
+ * Throws an exception if:
+ * - The values of 'autocommit' and/or 'startTransaction' are inconsistent with the current
+ * state of the transaction.
+ */
+ void beginOrContinue(TxnNumber txnNumber,
+ boost::optional<bool> autocommit,
+ boost::optional<bool> startTransaction);
+
+ static Status isValid(StringData dbName, StringData cmdName);
+
+ void transitionToPreparedforTest() {
+ stdx::lock_guard<stdx::mutex> lk(_mutex);
+ _txnState.transitionTo(lk, TransactionState::kPrepared);
+ }
+
+ void transitionToCommittingforTest() {
+ stdx::lock_guard<stdx::mutex> lk(_mutex);
+ _txnState.transitionTo(lk, TransactionState::kCommittingWithoutPrepare);
+ }
+
+ /**
+ * Checks to see if the txnNumber changed in the parent session and perform the necessary
+ * cleanup.
+ */
+ void checkForNewTxnNumber();
+
private:
+ /**
+ * Reserves a slot in the oplog with an open storage-transaction while it is alive. Reserves the
+ * slot at construction. Aborts the storage-transaction and releases the oplog slot at
+ * destruction.
+ */
+ class OplogSlotReserver {
+ public:
+ OplogSlotReserver(OperationContext* opCtx);
+
+ ~OplogSlotReserver();
+
+ // Rule of 5: because we have a class-defined destructor, we need to explictly specify
+ // the move operator and move assignment operator.
+ OplogSlotReserver(OplogSlotReserver&&) = default;
+ OplogSlotReserver& operator=(OplogSlotReserver&&) = default;
+
+ /**
+ * Returns the oplog slot reserved at construction.
+ */
+ OplogSlot getReservedOplogSlot() const {
+ invariant(!_oplogSlot.opTime.isNull());
+ return _oplogSlot;
+ }
+
+ private:
+ std::unique_ptr<Locker> _locker;
+ std::unique_ptr<RecoveryUnit> _recoveryUnit;
+ OplogSlot _oplogSlot;
+ };
+
+ /**
+ * Indicates the state of the current multi-document transaction, if any. If the transaction is
+ * in any state but kInProgress, no more operations can be collected. Once the transaction is in
+ * kPrepared, the transaction is not allowed to abort outside of an 'abortTransaction' command.
+ * At this point, aborting the transaction must log an 'abortTransaction' oplog entry.
+ */
+ class TransactionState {
+ public:
+ enum StateFlag {
+ kNone = 1 << 0,
+ kInProgress = 1 << 1,
+ kPrepared = 1 << 2,
+ kCommittingWithoutPrepare = 1 << 3,
+ kCommittingWithPrepare = 1 << 4,
+ kCommitted = 1 << 5,
+ kAborted = 1 << 6
+ };
+
+ using StateSet = int;
+ bool isInSet(WithLock, StateSet stateSet) const {
+ return _state & stateSet;
+ }
+
+ /**
+ * Transitions the session from the current state to the new state. If transition validation
+ * is not relaxed, invariants if the transition is illegal.
+ */
+ enum class TransitionValidation { kValidateTransition, kRelaxTransitionValidation };
+ void transitionTo(
+ WithLock,
+ StateFlag newState,
+ TransitionValidation shouldValidate = TransitionValidation::kValidateTransition);
+
+ bool inMultiDocumentTransaction(WithLock) const {
+ return _state == kInProgress || _state == kPrepared;
+ }
+
+ bool isNone(WithLock) const {
+ return _state == kNone;
+ }
+
+ bool isInProgress(WithLock) const {
+ return _state == kInProgress;
+ }
+
+ bool isPrepared(WithLock) const {
+ return _state == kPrepared;
+ }
+
+ bool isCommittingWithoutPrepare(WithLock) const {
+ return _state == kCommittingWithoutPrepare;
+ }
+
+ bool isCommittingWithPrepare(WithLock) const {
+ return _state == kCommittingWithPrepare;
+ }
+
+ bool isCommitted(WithLock) const {
+ return _state == kCommitted;
+ }
+
+ bool isAborted(WithLock) const {
+ return _state == kAborted;
+ }
+
+ std::string toString() const {
+ return toString(_state);
+ }
+
+ static std::string toString(StateFlag state);
+
+ private:
+ static bool _isLegalTransition(StateFlag oldState, StateFlag newState);
+
+ StateFlag _state = kNone;
+ };
+
+ friend std::ostream& operator<<(std::ostream& s, TransactionState txnState) {
+ return (s << txnState.toString());
+ }
+
+ friend StringBuilder& operator<<(StringBuilder& s, TransactionState txnState) {
+ return (s << txnState.toString());
+ }
+
+ // Committing a transaction first changes its state to "Committing*" and writes to the oplog,
+ // then it changes the state to "Committed".
+ //
+ // When a transaction is in "Committing" state, it's not allowed for other threads to change
+ // its state (i.e. abort the transaction), otherwise the on-disk state will diverge from the
+ // in-memory state.
+ // There are 3 cases where the transaction will be aborted.
+ // 1) abortTransaction command. Session check-out mechanism only allows one client to access a
+ // transaction.
+ // 2) killSession, stepdown, transaction timeout and any thread that aborts the transaction
+ // outside of session checkout. They can safely skip the committing transactions.
+ // 3) Migration. Should be able to skip committing transactions.
+ void _commitTransaction(stdx::unique_lock<stdx::mutex> lk, OperationContext* opCtx);
+
+ // Abort the transaction if it's in one of the expected states and clean up the transaction
+ // states associated with the opCtx.
+ void _abortActiveTransaction(OperationContext* opCtx,
+ TransactionState::StateSet expectedStates);
+
+ void _abortArbitraryTransaction(WithLock);
+
+ // Releases stashed transaction resources to abort the transaction on the session.
+ void _abortTransactionOnSession(WithLock);
+
+ // Clean up the transaction resources unstashed on operation context.
+ void _cleanUpTxnResourceOnOpCtx(WithLock wl, OperationContext* opCtx);
+
+ // Checks if the current transaction number of this transaction still matches with the
+ // parent session as well as the transaction number of the current operation context.
+ void _checkIsActiveTransaction(WithLock,
+ const TxnNumber& requestTxnNumber,
+ bool checkAbort) const;
+
+ // Logs the transaction information if it has run slower than the global parameter slowMS. The
+ // transaction must be committed or aborted when this function is called.
+ void _logSlowTransaction(WithLock wl,
+ const SingleThreadedLockStats* lockStats,
+ TransactionState::StateFlag terminationCause,
+ repl::ReadConcernArgs readConcernArgs);
+
+ // This method returns a string with information about a slow transaction. The format of the
+ // logging string produced should match the format used for slow operation logging. A
+ // transaction must be completed (committed or aborted) and a valid LockStats reference must be
+ // passed in order for this method to be called.
+ std::string _transactionInfoForLog(const SingleThreadedLockStats* lockStats,
+ TransactionState::StateFlag terminationCause,
+ repl::ReadConcernArgs readConcernArgs);
+
+ // Reports transaction stats for both active and inactive transactions using the provided
+ // builder.
+ void _reportTransactionStats(WithLock wl,
+ BSONObjBuilder* builder,
+ repl::ReadConcernArgs readConcernArgs) const;
+
+ void _updateState(WithLock wl, const Session::RefreshState& newState);
+
+ // Bumps up the transaction number of this transaction and perform the necessary cleanup.
+ void _setNewTxnNumber(WithLock wl, const TxnNumber& txnNumber);
+
+ // Returns the session that this transaction belongs to.
+ const Session* _getSession() const;
+ Session* _getSession();
+
+ // Protects the member variables below.
+ mutable stdx::mutex _mutex;
+
+ // Holds transaction resources between network operations.
+ boost::optional<TxnResources> _txnResourceStash;
+
+ // Maintains the transaction state and the transition table for legal state transitions.
+ TransactionState _txnState;
+
StateMachine _stateMachine;
+
+ // Holds oplog data for operations which have been applied in the current multi-document
+ // transaction.
+ std::vector<repl::ReplOperation> _transactionOperations;
+
+ // Total size in bytes of all operations within the _transactionOperations vector.
+ size_t _transactionOperationBytes = 0;
+
+ // This is the txnNumber that this transaction is actively working on. It can be different from
+ // the current txnNumber of the parent session (since it can be changed in couple of ways, like
+ // migration). In which case, it should make the necessary steps to also bump this number, like
+ // aborting the current transaction.
+ TxnNumber _activeTxnNumber{kUninitializedTxnNumber};
+
+ // Set when a snapshot read / transaction begins. Alleviates cache pressure by limiting how long
+ // a snapshot will remain open and available. Checked in combination with _txnState to determine
+ // whether the transaction should be aborted.
+ // This is unset until a transaction begins on the session, and then reset only when new
+ // transactions begin.
+ boost::optional<Date_t> _transactionExpireDate;
+
+ // The autoCommit setting of this transaction. Should always be false for multi-statement
+ // transaction. Currently only needed for diagnostics reporting.
+ boost::optional<bool> _autoCommit;
+
+ // The OpTime a speculative transaction is reading from and also the earliest opTime it
+ // should wait for write concern for on commit.
+ repl::OpTime _speculativeTransactionReadOpTime;
+
+ std::vector<MultikeyPathInfo> _multikeyPathInfo;
+
+ // Tracks metrics for a single multi-document transaction.
+ SingleTransactionStats _singleTransactionStats;
+
+ // Remembers the refresh count this object has read from Session.
+ long long _lastStateRefreshCount{0};
};
inline StringBuilder& operator<<(StringBuilder& sb,
diff --git a/src/mongo/db/transaction_participant_test.cpp b/src/mongo/db/transaction_participant_test.cpp
new file mode 100644
index 00000000000..6498e4797b3
--- /dev/null
+++ b/src/mongo/db/transaction_participant_test.cpp
@@ -0,0 +1,2438 @@
+/**
+ * Copyright (C) 2018 MongoDB, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * As a special exception, the copyright holders give permission to link the
+ * code of portions of this program with the OpenSSL library under certain
+ * conditions as described in each individual source file and distribute
+ * linked combinations including the program with the OpenSSL library. You
+ * must comply with the GNU Affero General Public License in all respects for
+ * all of the code used other than as permitted herein. If you modify file(s)
+ * with this exception, you may extend this exception to your version of the
+ * file(s), but you are not obligated to do so. If you do not wish to do so,
+ * delete this exception statement from your version. If you delete this
+ * exception statement from all source files in the program, then also delete
+ * it in the license file.
+ */
+
+#include "mongo/platform/basic.h"
+
+#include "mongo/db/client.h"
+#include "mongo/db/db_raii.h"
+#include "mongo/db/dbdirectclient.h"
+#include "mongo/db/op_observer_noop.h"
+#include "mongo/db/op_observer_registry.h"
+#include "mongo/db/operation_context.h"
+#include "mongo/db/operation_context_session_mongod.h"
+#include "mongo/db/repl/mock_repl_coord_server_fixture.h"
+#include "mongo/db/repl/oplog.h"
+#include "mongo/db/repl/oplog_entry.h"
+#include "mongo/db/repl/optime.h"
+#include "mongo/db/server_transactions_metrics.h"
+#include "mongo/db/service_context.h"
+#include "mongo/db/session_catalog.h"
+#include "mongo/db/stats/fill_locker_info.h"
+#include "mongo/db/transaction_participant.h"
+#include "mongo/stdx/future.h"
+#include "mongo/stdx/memory.h"
+#include "mongo/unittest/death_test.h"
+#include "mongo/unittest/unittest.h"
+#include "mongo/util/net/socket_utils.h"
+
+namespace mongo {
+namespace {
+
+const NamespaceString kNss("TestDB", "TestColl");
+const OptionalCollectionUUID kUUID;
+
+/**
+ * Creates an OplogEntry with given parameters and preset defaults for this test suite.
+ */
+repl::OplogEntry makeOplogEntry(repl::OpTime opTime,
+ repl::OpTypeEnum opType,
+ BSONObj object,
+ OperationSessionInfo sessionInfo,
+ boost::optional<Date_t> wallClockTime,
+ boost::optional<StmtId> stmtId,
+ boost::optional<repl::OpTime> prevWriteOpTimeInTransaction) {
+ return repl::OplogEntry(
+ opTime, // optime
+ 0, // hash
+ opType, // opType
+ kNss, // namespace
+ boost::none, // uuid
+ boost::none, // fromMigrate
+ 0, // version
+ object, // o
+ boost::none, // o2
+ sessionInfo, // sessionInfo
+ boost::none, // upsert
+ wallClockTime, // wall clock time
+ stmtId, // statement id
+ prevWriteOpTimeInTransaction, // optime of previous write within same transaction
+ boost::none, // pre-image optime
+ boost::none); // post-image optime
+}
+
+class OpObserverMock : public OpObserverNoop {
+public:
+ void onTransactionPrepare(OperationContext* opCtx, const OplogSlot& prepareOpTime) override;
+
+ bool onTransactionPrepareThrowsException = false;
+ bool transactionPrepared = false;
+ stdx::function<void()> onTransactionPrepareFn = [this]() { transactionPrepared = true; };
+
+ void onTransactionCommit(OperationContext* opCtx, bool wasPrepared) override;
+ bool onTransactionCommitThrowsException = false;
+ bool transactionCommitted = false;
+ stdx::function<void(bool)> onTransactionCommitFn = [this](bool wasPrepared) {
+ transactionCommitted = true;
+ };
+};
+
+void OpObserverMock::onTransactionPrepare(OperationContext* opCtx, const OplogSlot& prepareOpTime) {
+ ASSERT_TRUE(opCtx->lockState()->inAWriteUnitOfWork());
+ OpObserverNoop::onTransactionPrepare(opCtx, prepareOpTime);
+
+ uassert(ErrorCodes::OperationFailed,
+ "onTransactionPrepare() failed",
+ !onTransactionPrepareThrowsException);
+
+ onTransactionPrepareFn();
+}
+
+void OpObserverMock::onTransactionCommit(OperationContext* opCtx, bool wasPrepared) {
+ ASSERT_TRUE(opCtx->lockState()->inAWriteUnitOfWork());
+ OpObserverNoop::onTransactionCommit(opCtx, wasPrepared);
+ uassert(ErrorCodes::OperationFailed,
+ "onTransactionCommit() failed",
+ !onTransactionCommitThrowsException);
+ onTransactionCommitFn(wasPrepared);
+}
+
+// When this class is in scope, makes the system behave as if we're in a DBDirectClient
+class DirectClientSetter {
+public:
+ explicit DirectClientSetter(OperationContext* opCtx)
+ : _opCtx(opCtx), _wasInDirectClient(opCtx->getClient()->isInDirectClient()) {
+ opCtx->getClient()->setInDirectClient(true);
+ }
+
+ ~DirectClientSetter() {
+ _opCtx->getClient()->setInDirectClient(_wasInDirectClient);
+ }
+
+private:
+ const OperationContext* _opCtx;
+ const bool _wasInDirectClient;
+};
+
+class TxnParticipantTest : public MockReplCoordServerFixture {
+protected:
+ void setUp() final {
+ MockReplCoordServerFixture::setUp();
+
+ auto service = opCtx()->getServiceContext();
+ SessionCatalog::get(service)->onStepUp(opCtx());
+
+ OpObserverRegistry* opObserverRegistry =
+ dynamic_cast<OpObserverRegistry*>(service->getOpObserver());
+ auto mockObserver = stdx::make_unique<OpObserverMock>();
+ _opObserver = mockObserver.get();
+ opObserverRegistry->addObserver(std::move(mockObserver));
+
+ _sessionId = makeLogicalSessionIdForTest();
+ _txnNumber = 20;
+
+ opCtx()->setLogicalSessionId(_sessionId);
+ opCtx()->setTxnNumber(_txnNumber);
+ }
+
+ void tearDown() final {
+ // Clear all sessions to free up any stashed resources.
+ SessionCatalog::get(opCtx()->getServiceContext())->reset_forTest();
+
+ MockReplCoordServerFixture::tearDown();
+ _opObserver = nullptr;
+ }
+
+ SessionCatalog* catalog() {
+ return SessionCatalog::get(opCtx()->getServiceContext());
+ }
+
+ void runFunctionFromDifferentOpCtx(std::function<void(OperationContext*)> func) {
+ // Stash the original client.
+ auto originalClient = Client::releaseCurrent();
+
+ // Create a new client (e.g. for migration) and opCtx.
+ auto service = opCtx()->getServiceContext();
+ auto newClientOwned = service->makeClient("newClient");
+ auto newClient = newClientOwned.get();
+ Client::setCurrent(std::move(newClientOwned));
+ auto newOpCtx = newClient->makeOperationContext();
+
+ // Run the function on bahalf of another operation context.
+ func(newOpCtx.get());
+
+ // Restore the original client.
+ newOpCtx.reset();
+ Client::releaseCurrent();
+ Client::setCurrent(std::move(originalClient));
+ }
+
+ void bumpTxnNumberFromDifferentOpCtx(const LogicalSessionId& sessionId, TxnNumber newTxnNum) {
+ auto func = [sessionId, newTxnNum](OperationContext* opCtx) {
+
+ auto session = SessionCatalog::get(opCtx)->getOrCreateSession(opCtx, sessionId);
+ auto txnParticipant =
+ TransactionParticipant::getFromNonCheckedOutSession(session.get());
+
+ // Check that there is a transaction in progress with a lower txnNumber.
+ ASSERT(txnParticipant->inMultiDocumentTransaction());
+ ASSERT_LT(session->getActiveTxnNumber(), newTxnNum);
+
+ // Check that the transaction has some operations, so we can ensure they are cleared.
+ ASSERT_GT(txnParticipant->transactionOperationsForTest().size(), 0u);
+
+ // Bump the active transaction number on the txnParticipant. This should clear all state
+ // from the previous transaction.
+ session->beginOrContinueTxn(opCtx, newTxnNum);
+ ASSERT_EQ(session->getActiveTxnNumber(), newTxnNum);
+
+ txnParticipant->checkForNewTxnNumber();
+ ASSERT_FALSE(txnParticipant->transactionIsAborted());
+ ASSERT_EQ(txnParticipant->transactionOperationsForTest().size(), 0u);
+ };
+
+ runFunctionFromDifferentOpCtx(func);
+ }
+
+ OpObserverMock* _opObserver = nullptr;
+ LogicalSessionId _sessionId;
+ TxnNumber _txnNumber;
+};
+
+// Test that transaction lock acquisition times out in `maxTransactionLockRequestTimeoutMillis`
+// milliseconds.
+TEST_F(TxnParticipantTest, TransactionThrowsLockTimeoutIfLockIsUnavailable) {
+ const std::string dbName = "TestDB";
+
+ OperationContextSessionMongod opCtxSession(opCtx(), true, false, true);
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+ txnParticipant->unstashTransactionResources(opCtx(), "insert");
+
+ { Lock::DBLock dbXLock(opCtx(), dbName, MODE_X); }
+ txnParticipant->stashTransactionResources(opCtx());
+ auto clientWithDatabaseXLock = Client::releaseCurrent();
+
+
+ /**
+ * Make a new Session, Client, OperationContext and transaction and then attempt to take the
+ * same database exclusive lock, which should conflict because the other transaction already
+ * took it.
+ */
+
+ auto service = opCtx()->getServiceContext();
+ auto newClientOwned = service->makeClient("newTransactionClient");
+ auto newClient = newClientOwned.get();
+ Client::setCurrent(std::move(newClientOwned));
+
+ const auto newSessionId = makeLogicalSessionIdForTest();
+ const TxnNumber newTxnNum = 10;
+ {
+ // Limit the scope of the new opCtx to make sure that it gets destroyed before
+ // new client is destroyed.
+ auto newOpCtx = newClient->makeOperationContext();
+ newOpCtx.get()->setLogicalSessionId(newSessionId);
+ newOpCtx.get()->setTxnNumber(newTxnNum);
+
+ OperationContextSessionMongod newOpCtxSession(newOpCtx.get(), true, false, true);
+
+ auto newTxnParticipant = TransactionParticipant::get(newOpCtx.get());
+ newTxnParticipant->unstashTransactionResources(newOpCtx.get(), "insert");
+
+ Date_t t1 = Date_t::now();
+ ASSERT_THROWS_CODE(Lock::DBLock(newOpCtx.get(), dbName, MODE_X),
+ AssertionException,
+ ErrorCodes::LockTimeout);
+ Date_t t2 = Date_t::now();
+ int defaultMaxTransactionLockRequestTimeoutMillis = 5;
+ ASSERT_GTE(t2 - t1, Milliseconds(defaultMaxTransactionLockRequestTimeoutMillis));
+
+ // A non-conflicting lock acquisition should work just fine.
+ { Lock::DBLock tempLock(newOpCtx.get(), "NewTestDB", MODE_X); }
+ }
+ // Restore the original client so that teardown works.
+ Client::releaseCurrent();
+ Client::setCurrent(std::move(clientWithDatabaseXLock));
+}
+
+TEST_F(TxnParticipantTest, StashAndUnstashResources) {
+ Locker* originalLocker = opCtx()->lockState();
+ RecoveryUnit* originalRecoveryUnit = opCtx()->recoveryUnit();
+ ASSERT(originalLocker);
+ ASSERT(originalRecoveryUnit);
+
+ OperationContextSessionMongod opCtxSession(opCtx(), true, false, true);
+
+
+ repl::ReadConcernArgs readConcernArgs;
+ ASSERT_OK(readConcernArgs.initialize(BSON("find"
+ << "test"
+ << repl::ReadConcernArgs::kReadConcernFieldName
+ << BSON(repl::ReadConcernArgs::kLevelFieldName
+ << "snapshot"))));
+ repl::ReadConcernArgs::get(opCtx()) = readConcernArgs;
+
+ // Perform initial unstash which sets up a WriteUnitOfWork.
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+ txnParticipant->unstashTransactionResources(opCtx(), "find");
+ ASSERT_EQUALS(originalLocker, opCtx()->lockState());
+ ASSERT_EQUALS(originalRecoveryUnit, opCtx()->recoveryUnit());
+ ASSERT(opCtx()->getWriteUnitOfWork());
+ ASSERT(opCtx()->lockState()->isLocked());
+
+ // Stash resources. The original Locker and RecoveryUnit now belong to the stash.
+ txnParticipant->stashTransactionResources(opCtx());
+ ASSERT_NOT_EQUALS(originalLocker, opCtx()->lockState());
+ ASSERT_NOT_EQUALS(originalRecoveryUnit, opCtx()->recoveryUnit());
+ ASSERT(!opCtx()->getWriteUnitOfWork());
+
+ // Unset the read concern on the OperationContext. This is needed to unstash.
+ repl::ReadConcernArgs::get(opCtx()) = repl::ReadConcernArgs();
+
+ // Unstash the stashed resources. This restores the original Locker and RecoveryUnit to the
+ // OperationContext.
+ txnParticipant->unstashTransactionResources(opCtx(), "find");
+ ASSERT_EQUALS(originalLocker, opCtx()->lockState());
+ ASSERT_EQUALS(originalRecoveryUnit, opCtx()->recoveryUnit());
+ ASSERT(opCtx()->getWriteUnitOfWork());
+
+ // Commit the transaction. This allows us to release locks.
+ txnParticipant->commitUnpreparedTransaction(opCtx());
+}
+
+TEST_F(TxnParticipantTest, ReportStashedResources) {
+ Date_t startTime = Date_t::now();
+ const bool autocommit = false;
+
+ ASSERT(opCtx()->lockState());
+ ASSERT(opCtx()->recoveryUnit());
+
+ OperationContextSessionMongod opCtxSession(opCtx(), true, autocommit, true);
+
+ // Create a ClientMetadata object and set it on ClientMetadataIsMasterState.
+ BSONObjBuilder builder;
+ ASSERT_OK(ClientMetadata::serializePrivate("driverName",
+ "driverVersion",
+ "osType",
+ "osName",
+ "osArchitecture",
+ "osVersion",
+ "appName",
+ &builder));
+ auto obj = builder.obj();
+ auto clientMetadata = ClientMetadata::parse(obj["client"]);
+ auto& clientMetadataIsMasterState = ClientMetadataIsMasterState::get(opCtx()->getClient());
+ clientMetadataIsMasterState.setClientMetadata(opCtx()->getClient(),
+ std::move(clientMetadata.getValue()));
+
+ repl::ReadConcernArgs readConcernArgs;
+ ASSERT_OK(readConcernArgs.initialize(BSON("find"
+ << "test"
+ << repl::ReadConcernArgs::kReadConcernFieldName
+ << BSON(repl::ReadConcernArgs::kLevelFieldName
+ << "snapshot"))));
+ repl::ReadConcernArgs::get(opCtx()) = readConcernArgs;
+
+ // Perform initial unstash which sets up a WriteUnitOfWork.
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+ txnParticipant->unstashTransactionResources(opCtx(), "find");
+ ASSERT(opCtx()->getWriteUnitOfWork());
+ ASSERT(opCtx()->lockState()->isLocked());
+
+ // Stash resources. The original Locker and RecoveryUnit now belong to the stash.
+ txnParticipant->stashTransactionResources(opCtx());
+ ASSERT(!opCtx()->getWriteUnitOfWork());
+
+ // Verify that the Session's report of its own stashed state aligns with our expectations.
+ auto stashedState = txnParticipant->reportStashedState();
+ auto transactionDocument = stashedState.getObjectField("transaction");
+ auto parametersDocument = transactionDocument.getObjectField("parameters");
+
+ ASSERT_EQ(stashedState.getField("host").valueStringData().toString(),
+ getHostNameCachedAndPort());
+ ASSERT_EQ(stashedState.getField("desc").valueStringData().toString(), "inactive transaction");
+ ASSERT_BSONOBJ_EQ(stashedState.getField("lsid").Obj(), _sessionId.toBSON());
+ ASSERT_EQ(parametersDocument.getField("txnNumber").numberLong(), *opCtx()->getTxnNumber());
+ ASSERT_EQ(parametersDocument.getField("autocommit").boolean(), autocommit);
+ ASSERT_BSONELT_EQ(parametersDocument.getField("readConcern"),
+ readConcernArgs.toBSON().getField("readConcern"));
+ ASSERT_GTE(transactionDocument.getField("readTimestamp").timestamp(), Timestamp(0, 0));
+ ASSERT_GTE(
+ dateFromISOString(transactionDocument.getField("startWallClockTime").valueStringData())
+ .getValue(),
+ startTime);
+ ASSERT_EQ(
+ dateFromISOString(transactionDocument.getField("expiryTime").valueStringData()).getValue(),
+ Date_t::fromMillisSinceEpoch(txnParticipant->getSingleTransactionStats().getStartTime() /
+ 1000) +
+ stdx::chrono::seconds{transactionLifetimeLimitSeconds.load()});
+
+ ASSERT_EQ(stashedState.getField("client").valueStringData().toString(), "");
+ ASSERT_EQ(stashedState.getField("connectionId").numberLong(), 0);
+ ASSERT_EQ(stashedState.getField("appName").valueStringData().toString(), "appName");
+ ASSERT_BSONOBJ_EQ(stashedState.getField("clientMetadata").Obj(), obj.getField("client").Obj());
+ ASSERT_EQ(stashedState.getField("waitingForLock").boolean(), false);
+ ASSERT_EQ(stashedState.getField("active").boolean(), false);
+
+ // For the following time metrics, we are only verifying that the transaction sub-document is
+ // being constructed correctly with proper types because we have other tests to verify that the
+ // values are being tracked correctly.
+ ASSERT_GTE(transactionDocument.getField("timeOpenMicros").numberLong(), 0);
+ ASSERT_GTE(transactionDocument.getField("timeActiveMicros").numberLong(), 0);
+ ASSERT_GTE(transactionDocument.getField("timeInactiveMicros").numberLong(), 0);
+
+ // Unset the read concern on the OperationContext. This is needed to unstash.
+ repl::ReadConcernArgs::get(opCtx()) = repl::ReadConcernArgs();
+
+ // Unstash the stashed resources. This restores the original Locker and RecoveryUnit to the
+ // OperationContext.
+ txnParticipant->unstashTransactionResources(opCtx(), "commitTransaction");
+ ASSERT(opCtx()->getWriteUnitOfWork());
+
+ // With the resources unstashed, verify that the Session reports an empty stashed state.
+ ASSERT(txnParticipant->reportStashedState().isEmpty());
+
+ // Commit the transaction. This allows us to release locks.
+ txnParticipant->commitUnpreparedTransaction(opCtx());
+}
+
+TEST_F(TxnParticipantTest, ReportUnstashedResources) {
+ Date_t startTime = Date_t::now();
+ ASSERT(opCtx()->lockState());
+ ASSERT(opCtx()->recoveryUnit());
+
+ const auto autocommit = false;
+ OperationContextSessionMongod opCtxSession(opCtx(), true, autocommit, true);
+
+ repl::ReadConcernArgs readConcernArgs;
+ ASSERT_OK(readConcernArgs.initialize(BSON("find"
+ << "test"
+ << repl::ReadConcernArgs::kReadConcernFieldName
+ << BSON(repl::ReadConcernArgs::kLevelFieldName
+ << "snapshot"))));
+ repl::ReadConcernArgs::get(opCtx()) = readConcernArgs;
+
+ // Perform initial unstash which sets up a WriteUnitOfWork.
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+ txnParticipant->unstashTransactionResources(opCtx(), "find");
+ ASSERT(opCtx()->getWriteUnitOfWork());
+ ASSERT(opCtx()->lockState()->isLocked());
+
+ // Verify that the Session's report of its own unstashed state aligns with our expectations.
+ BSONObjBuilder unstashedStateBuilder;
+ txnParticipant->reportUnstashedState(repl::ReadConcernArgs::get(opCtx()),
+ &unstashedStateBuilder);
+ auto unstashedState = unstashedStateBuilder.obj();
+ auto transactionDocument = unstashedState.getObjectField("transaction");
+ auto parametersDocument = transactionDocument.getObjectField("parameters");
+
+ ASSERT_EQ(parametersDocument.getField("txnNumber").numberLong(), *opCtx()->getTxnNumber());
+ ASSERT_EQ(parametersDocument.getField("autocommit").boolean(), autocommit);
+ ASSERT_BSONELT_EQ(parametersDocument.getField("readConcern"),
+ readConcernArgs.toBSON().getField("readConcern"));
+ ASSERT_GTE(transactionDocument.getField("readTimestamp").timestamp(), Timestamp(0, 0));
+ ASSERT_GTE(
+ dateFromISOString(transactionDocument.getField("startWallClockTime").valueStringData())
+ .getValue(),
+ startTime);
+ ASSERT_EQ(
+ dateFromISOString(transactionDocument.getField("expiryTime").valueStringData()).getValue(),
+ Date_t::fromMillisSinceEpoch(txnParticipant->getSingleTransactionStats().getStartTime() /
+ 1000) +
+ stdx::chrono::seconds{transactionLifetimeLimitSeconds.load()});
+
+ // For the following time metrics, we are only verifying that the transaction sub-document is
+ // being constructed correctly with proper types because we have other tests to verify that the
+ // values are being tracked correctly.
+ ASSERT_GTE(transactionDocument.getField("timeOpenMicros").numberLong(), 0);
+ ASSERT_GTE(transactionDocument.getField("timeActiveMicros").numberLong(), 0);
+ ASSERT_GTE(transactionDocument.getField("timeInactiveMicros").numberLong(), 0);
+
+ // Stash resources. The original Locker and RecoveryUnit now belong to the stash.
+ txnParticipant->stashTransactionResources(opCtx());
+ ASSERT(!opCtx()->getWriteUnitOfWork());
+
+ // With the resources stashed, verify that the Session reports an empty unstashed state.
+ BSONObjBuilder builder;
+ txnParticipant->reportUnstashedState(repl::ReadConcernArgs::get(opCtx()), &builder);
+ ASSERT(builder.obj().isEmpty());
+}
+
+TEST_F(TxnParticipantTest, ReportUnstashedResourcesForARetryableWrite) {
+ ASSERT(opCtx()->lockState());
+ ASSERT(opCtx()->recoveryUnit());
+
+ OperationContextSessionMongod opCtxSession(opCtx(), true, boost::none, boost::none);
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+ txnParticipant->unstashTransactionResources(opCtx(), "find");
+
+ // Build a BSONObj containing the details which we expect to see reported when we call
+ // Session::reportUnstashedState. For a retryable write, we should only include the txnNumber.
+ BSONObjBuilder reportBuilder;
+ BSONObjBuilder transactionBuilder(reportBuilder.subobjStart("transaction"));
+ BSONObjBuilder parametersBuilder(transactionBuilder.subobjStart("parameters"));
+ parametersBuilder.append("txnNumber", *opCtx()->getTxnNumber());
+ parametersBuilder.done();
+ transactionBuilder.done();
+
+ // Verify that the Session's report of its own unstashed state aligns with our expectations.
+ BSONObjBuilder unstashedStateBuilder;
+ txnParticipant->reportUnstashedState(repl::ReadConcernArgs::get(opCtx()),
+ &unstashedStateBuilder);
+ ASSERT_BSONOBJ_EQ(unstashedStateBuilder.obj(), reportBuilder.obj());
+}
+
+TEST_F(TxnParticipantTest, CannotSpecifyStartTransactionOnInProgressTxn) {
+ // Must specify startTransaction=true and autocommit=false to start a transaction.
+ OperationContextSessionMongod opCtxSession(opCtx(), true, false, true);
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+ ASSERT_TRUE(txnParticipant->inMultiDocumentTransaction());
+
+ // Cannot try to start a transaction that already started.
+ ASSERT_THROWS_CODE(txnParticipant->beginOrContinue(*opCtx()->getTxnNumber(), false, true),
+ AssertionException,
+ ErrorCodes::ConflictingOperationInProgress);
+}
+
+TEST_F(TxnParticipantTest, AutocommitRequiredOnEveryTxnOp) {
+ OperationContextSessionMongod opCtxSession(opCtx(), true, false, true);
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+
+ // We must have stashed transaction resources to do a second operation on the transaction.
+ txnParticipant->unstashTransactionResources(opCtx(), "insert");
+ // The transaction machinery cannot store an empty locker.
+ { Lock::GlobalLock lk(opCtx(), MODE_IX, Date_t::now(), Lock::InterruptBehavior::kThrow); }
+ txnParticipant->stashTransactionResources(opCtx());
+
+ auto txnNum = *opCtx()->getTxnNumber();
+ // Omitting 'autocommit' after the first statement of a transaction should throw an error.
+ ASSERT_THROWS_CODE(txnParticipant->beginOrContinue(txnNum, boost::none, boost::none),
+ AssertionException,
+ ErrorCodes::InvalidOptions);
+
+ // Setting 'autocommit=true' should throw an error.
+ ASSERT_THROWS_CODE(txnParticipant->beginOrContinue(txnNum, true, boost::none),
+ AssertionException,
+ ErrorCodes::InvalidOptions);
+
+ // Including autocommit=false should succeed.
+ txnParticipant->beginOrContinue(*opCtx()->getTxnNumber(), false, boost::none);
+}
+
+TEST_F(TxnParticipantTest, SameTransactionPreservesStoredStatements) {
+ OperationContextSessionMongod opCtxSession(opCtx(), true, false, true);
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+
+ // We must have stashed transaction resources to re-open the transaction.
+ txnParticipant->unstashTransactionResources(opCtx(), "insert");
+ auto operation = repl::OplogEntry::makeInsertOperation(kNss, kUUID, BSON("TestValue" << 0));
+ txnParticipant->addTransactionOperation(opCtx(), operation);
+ ASSERT_BSONOBJ_EQ(operation.toBSON(),
+ txnParticipant->transactionOperationsForTest()[0].toBSON());
+ // The transaction machinery cannot store an empty locker.
+ { Lock::GlobalLock lk(opCtx(), MODE_IX, Date_t::now(), Lock::InterruptBehavior::kThrow); }
+ txnParticipant->stashTransactionResources(opCtx());
+
+ // Check the transaction operations before re-opening the transaction.
+ ASSERT_BSONOBJ_EQ(operation.toBSON(),
+ txnParticipant->transactionOperationsForTest()[0].toBSON());
+
+ // Re-opening the same transaction should have no effect.
+ txnParticipant->beginOrContinue(*opCtx()->getTxnNumber(), false, boost::none);
+ ASSERT_BSONOBJ_EQ(operation.toBSON(),
+ txnParticipant->transactionOperationsForTest()[0].toBSON());
+}
+
+TEST_F(TxnParticipantTest, AbortClearsStoredStatements) {
+ OperationContextSessionMongod opCtxSession(opCtx(), true, false, true);
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+ txnParticipant->unstashTransactionResources(opCtx(), "insert");
+ auto operation = repl::OplogEntry::makeInsertOperation(kNss, kUUID, BSON("TestValue" << 0));
+ txnParticipant->addTransactionOperation(opCtx(), operation);
+ ASSERT_BSONOBJ_EQ(operation.toBSON(),
+ txnParticipant->transactionOperationsForTest()[0].toBSON());
+
+ // The transaction machinery cannot store an empty locker.
+ { Lock::GlobalLock lk(opCtx(), MODE_IX, Date_t::now(), Lock::InterruptBehavior::kThrow); }
+ txnParticipant->stashTransactionResources(opCtx());
+ txnParticipant->abortArbitraryTransaction();
+ ASSERT_TRUE(txnParticipant->transactionOperationsForTest().empty());
+ ASSERT_TRUE(txnParticipant->transactionIsAborted());
+}
+
+// This test makes sure the commit machinery works even when no operations are done on the
+// transaction.
+TEST_F(TxnParticipantTest, EmptyTransactionCommit) {
+ OperationContextSessionMongod opCtxSession(opCtx(), true, false, true);
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+ txnParticipant->unstashTransactionResources(opCtx(), "commitTransaction");
+
+ // The transaction machinery cannot store an empty locker.
+ Lock::GlobalLock lk(opCtx(), MODE_IX, Date_t::now(), Lock::InterruptBehavior::kThrow);
+ txnParticipant->commitUnpreparedTransaction(opCtx());
+ txnParticipant->stashTransactionResources(opCtx());
+
+ ASSERT_TRUE(txnParticipant->transactionIsCommitted());
+}
+
+TEST_F(TxnParticipantTest, CommitTransactionSetsCommitTimestampOnPreparedTransaction) {
+ OperationContextSessionMongod opCtxSession(opCtx(), true, false, true);
+
+ Timestamp actualCommitTimestamp;
+ auto originalFn = _opObserver->onTransactionCommitFn;
+ _opObserver->onTransactionCommitFn = [&](bool wasPrepared) {
+ originalFn(wasPrepared);
+ ASSERT(wasPrepared);
+ actualCommitTimestamp = opCtx()->recoveryUnit()->getCommitTimestamp();
+ };
+
+ const auto commitTimestamp = Timestamp(6, 6);
+
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+ txnParticipant->unstashTransactionResources(opCtx(), "commitTransaction");
+
+ // The transaction machinery cannot store an empty locker.
+ Lock::GlobalLock lk(opCtx(), MODE_IX, Date_t::now(), Lock::InterruptBehavior::kThrow);
+ txnParticipant->prepareTransaction(opCtx());
+ txnParticipant->commitPreparedTransaction(opCtx(), commitTimestamp);
+
+ ASSERT_EQ(commitTimestamp, actualCommitTimestamp);
+ // The recovery unit is reset on commit.
+ ASSERT(opCtx()->recoveryUnit()->getCommitTimestamp().isNull());
+
+ txnParticipant->stashTransactionResources(opCtx());
+ ASSERT_TRUE(txnParticipant->transactionIsCommitted());
+ ASSERT(opCtx()->recoveryUnit()->getCommitTimestamp().isNull());
+}
+
+TEST_F(TxnParticipantTest, CommitTransactionWithCommitTimestampFailsOnUnpreparedTransaction) {
+ const auto commitTimestamp = Timestamp(6, 6);
+
+ OperationContextSessionMongod opCtxSession(opCtx(), true, false, true);
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+ txnParticipant->unstashTransactionResources(opCtx(), "commitTransaction");
+
+ // The transaction machinery cannot store an empty locker.
+ Lock::GlobalLock lk(opCtx(), MODE_IX, Date_t::now(), Lock::InterruptBehavior::kThrow);
+ ASSERT_THROWS_CODE(txnParticipant->commitPreparedTransaction(opCtx(), commitTimestamp),
+ AssertionException,
+ ErrorCodes::InvalidOptions);
+}
+
+TEST_F(TxnParticipantTest, CommitTransactionDoesNotSetCommitTimestampOnUnpreparedTransaction) {
+ OperationContextSessionMongod opCtxSession(opCtx(), true, false, true);
+
+ Timestamp actualCommitTimestamp;
+ auto originalFn = _opObserver->onTransactionCommitFn;
+ _opObserver->onTransactionCommitFn = [&](bool wasPrepared) {
+ originalFn(wasPrepared);
+ ASSERT_FALSE(wasPrepared);
+ actualCommitTimestamp = opCtx()->recoveryUnit()->getCommitTimestamp();
+ };
+
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+ txnParticipant->unstashTransactionResources(opCtx(), "commitTransaction");
+
+ // The transaction machinery cannot store an empty locker.
+ Lock::GlobalLock lk(opCtx(), MODE_IX, Date_t::now(), Lock::InterruptBehavior::kThrow);
+ txnParticipant->commitUnpreparedTransaction(opCtx());
+
+ ASSERT(opCtx()->recoveryUnit()->getCommitTimestamp().isNull());
+ ASSERT(actualCommitTimestamp.isNull());
+
+ txnParticipant->stashTransactionResources(opCtx());
+ ASSERT_TRUE(txnParticipant->transactionIsCommitted());
+ ASSERT(opCtx()->recoveryUnit()->getCommitTimestamp().isNull());
+}
+
+TEST_F(TxnParticipantTest, CommitTransactionWithoutCommitTimestampFailsOnPreparedTransaction) {
+ OperationContextSessionMongod opCtxSession(opCtx(), true, false, true);
+
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+ txnParticipant->unstashTransactionResources(opCtx(), "commitTransaction");
+
+ // The transaction machinery cannot store an empty locker.
+ Lock::GlobalLock lk(opCtx(), MODE_IX, Date_t::now(), Lock::InterruptBehavior::kThrow);
+ txnParticipant->prepareTransaction(opCtx());
+ ASSERT_THROWS_CODE(txnParticipant->commitUnpreparedTransaction(opCtx()),
+ AssertionException,
+ ErrorCodes::InvalidOptions);
+}
+
+TEST_F(TxnParticipantTest, CommitTransactionWithNullCommitTimestampFailsOnPreparedTransaction) {
+ OperationContextSessionMongod opCtxSession(opCtx(), true, false, true);
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+
+ txnParticipant->unstashTransactionResources(opCtx(), "commitTransaction");
+
+ // The transaction machinery cannot store an empty locker.
+ Lock::GlobalLock lk(opCtx(), MODE_IX, Date_t::now(), Lock::InterruptBehavior::kThrow);
+ txnParticipant->prepareTransaction(opCtx());
+ ASSERT_THROWS_CODE(txnParticipant->commitPreparedTransaction(opCtx(), Timestamp()),
+ AssertionException,
+ ErrorCodes::InvalidOptions);
+}
+
+// This test makes sure the abort machinery works even when no operations are done on the
+// transaction.
+TEST_F(TxnParticipantTest, EmptyTransactionAbort) {
+ OperationContextSessionMongod opCtxSession(opCtx(), true, false, true);
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+ txnParticipant->unstashTransactionResources(opCtx(), "abortTransaction");
+
+ // The transaction machinery cannot store an empty locker.
+ { Lock::GlobalLock lk(opCtx(), MODE_IX, Date_t::now(), Lock::InterruptBehavior::kThrow); }
+ txnParticipant->stashTransactionResources(opCtx());
+ txnParticipant->abortArbitraryTransaction();
+ ASSERT_TRUE(txnParticipant->transactionIsAborted());
+}
+
+TEST_F(TxnParticipantTest, ConcurrencyOfUnstashAndAbort) {
+ OperationContextSessionMongod opCtxSession(opCtx(), true, false, true);
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+
+ // The transaction may be aborted without checking out the txnParticipant.
+ txnParticipant->abortArbitraryTransaction();
+
+ // An unstash after an abort should uassert.
+ ASSERT_THROWS_CODE(txnParticipant->unstashTransactionResources(opCtx(), "find"),
+ AssertionException,
+ ErrorCodes::NoSuchTransaction);
+}
+
+TEST_F(TxnParticipantTest, ConcurrencyOfUnstashAndMigration) {
+ OperationContextSessionMongod opCtxSession(opCtx(), true, false, true);
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+ txnParticipant->unstashTransactionResources(opCtx(), "insert");
+
+ // The transaction machinery cannot store an empty locker.
+ { Lock::GlobalLock lk(opCtx(), MODE_IX, Date_t::now(), Lock::InterruptBehavior::kThrow); }
+ auto operation = repl::OplogEntry::makeInsertOperation(kNss, kUUID, BSON("TestValue" << 0));
+ txnParticipant->addTransactionOperation(opCtx(), operation);
+ txnParticipant->stashTransactionResources(opCtx());
+
+ // A migration may bump the active transaction number without checking out the
+ // txnParticipant.
+ const auto higherTxnNum = *opCtx()->getTxnNumber() + 1;
+ bumpTxnNumberFromDifferentOpCtx(*opCtx()->getLogicalSessionId(), higherTxnNum);
+
+ // An unstash after a migration that bumps the active transaction number should uassert.
+ ASSERT_THROWS_CODE(txnParticipant->unstashTransactionResources(opCtx(), "insert"),
+ AssertionException,
+ ErrorCodes::ConflictingOperationInProgress);
+}
+
+TEST_F(TxnParticipantTest, ConcurrencyOfStashAndAbort) {
+ OperationContextSessionMongod opCtxSession(opCtx(), true, false, true);
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+ txnParticipant->unstashTransactionResources(opCtx(), "find");
+
+ // The transaction may be aborted without checking out the txnParticipant->
+ txnParticipant->abortArbitraryTransaction();
+
+ // A stash after an abort should be a noop.
+ txnParticipant->stashTransactionResources(opCtx());
+}
+
+TEST_F(TxnParticipantTest, ConcurrencyOfStashAndMigration) {
+ OperationContextSessionMongod opCtxSession(opCtx(), true, false, true);
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+
+ txnParticipant->unstashTransactionResources(opCtx(), "insert");
+ auto operation = repl::OplogEntry::makeInsertOperation(kNss, kUUID, BSON("TestValue" << 0));
+ txnParticipant->addTransactionOperation(opCtx(), operation);
+
+ // A migration may bump the active transaction number without checking out the
+ // txnParticipant.
+ const auto higherTxnNum = *opCtx()->getTxnNumber() + 1;
+ bumpTxnNumberFromDifferentOpCtx(*opCtx()->getLogicalSessionId(), higherTxnNum);
+
+ // A stash after a migration that bumps the active transaction number should uassert.
+ ASSERT_THROWS_CODE(txnParticipant->stashTransactionResources(opCtx()),
+ AssertionException,
+ ErrorCodes::ConflictingOperationInProgress);
+}
+
+TEST_F(TxnParticipantTest, ConcurrencyOfAddTransactionOperationAndAbort) {
+ OperationContextSessionMongod opCtxSession(opCtx(), true, false, true);
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+ txnParticipant->unstashTransactionResources(opCtx(), "insert");
+
+ // The transaction may be aborted without checking out the txnParticipant.
+ txnParticipant->abortArbitraryTransaction();
+
+ // An addTransactionOperation() after an abort should uassert.
+ auto operation = repl::OplogEntry::makeInsertOperation(kNss, kUUID, BSON("TestValue" << 0));
+ ASSERT_THROWS_CODE(txnParticipant->addTransactionOperation(opCtx(), operation),
+ AssertionException,
+ ErrorCodes::NoSuchTransaction);
+}
+
+TEST_F(TxnParticipantTest, ConcurrencyOfAddTransactionOperationAndMigration) {
+ OperationContextSessionMongod opCtxSession(opCtx(), true, false, true);
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+
+ txnParticipant->unstashTransactionResources(opCtx(), "find");
+ auto operation = repl::OplogEntry::makeInsertOperation(kNss, kUUID, BSON("TestValue" << 0));
+ txnParticipant->addTransactionOperation(opCtx(), operation);
+
+ // A migration may bump the active transaction number without checking out the
+ // txnParticipant.
+ const auto higherTxnNum = *opCtx()->getTxnNumber() + 1;
+ bumpTxnNumberFromDifferentOpCtx(*opCtx()->getLogicalSessionId(), higherTxnNum);
+
+ // An addTransactionOperation() after a migration that bumps the active transaction number
+ // should uassert.
+ ASSERT_THROWS_CODE(txnParticipant->addTransactionOperation(opCtx(), operation),
+ AssertionException,
+ ErrorCodes::ConflictingOperationInProgress);
+}
+
+TEST_F(TxnParticipantTest, ConcurrencyOfEndTransactionAndRetrieveOperationsAndAbort) {
+ OperationContextSessionMongod opCtxSession(opCtx(), true, false, true);
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+ txnParticipant->unstashTransactionResources(opCtx(), "insert");
+
+ // The transaction may be aborted without checking out the txnParticipant.
+ txnParticipant->abortArbitraryTransaction();
+
+ // An endTransactionAndRetrieveOperations() after an abort should uassert.
+ ASSERT_THROWS_CODE(txnParticipant->endTransactionAndRetrieveOperations(opCtx()),
+ AssertionException,
+ ErrorCodes::NoSuchTransaction);
+}
+
+TEST_F(TxnParticipantTest, ConcurrencyOfEndTransactionAndRetrieveOperationsAndMigration) {
+ OperationContextSessionMongod opCtxSession(opCtx(), true, false, true);
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+
+ txnParticipant->unstashTransactionResources(opCtx(), "insert");
+ auto operation = repl::OplogEntry::makeInsertOperation(kNss, kUUID, BSON("TestValue" << 0));
+ txnParticipant->addTransactionOperation(opCtx(), operation);
+
+ // A migration may bump the active transaction number without checking out the txnParticipant.
+ const auto higherTxnNum = *opCtx()->getTxnNumber() + 1;
+ bumpTxnNumberFromDifferentOpCtx(*opCtx()->getLogicalSessionId(), higherTxnNum);
+
+ // An endTransactionAndRetrieveOperations() after a migration that bumps the active transaction
+ // number should uassert.
+ ASSERT_THROWS_CODE(txnParticipant->endTransactionAndRetrieveOperations(opCtx()),
+ AssertionException,
+ ErrorCodes::ConflictingOperationInProgress);
+}
+
+TEST_F(TxnParticipantTest, ConcurrencyOfCommitTransactionAndAbort) {
+ OperationContextSessionMongod opCtxSession(opCtx(), true, false, true);
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+
+ txnParticipant->unstashTransactionResources(opCtx(), "commitTransaction");
+
+ // The transaction may be aborted without checking out the txnParticipant.
+ txnParticipant->abortArbitraryTransaction();
+
+ // An commitPreparedTransaction() after an abort should uassert.
+ ASSERT_THROWS_CODE(txnParticipant->commitUnpreparedTransaction(opCtx()),
+ AssertionException,
+ ErrorCodes::NoSuchTransaction);
+}
+
+TEST_F(TxnParticipantTest, ConcurrencyOfActiveAbortAndArbitraryAbort) {
+ OperationContextSessionMongod opCtxSession(opCtx(), true, false, true);
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+
+ txnParticipant->unstashTransactionResources(opCtx(), "insert");
+ ASSERT(txnParticipant->inMultiDocumentTransaction());
+
+ // The transaction may be aborted without checking out the txnParticipant.
+ txnParticipant->abortArbitraryTransaction();
+
+ // The operation throws for some reason and aborts implicitly.
+ // Abort active transaction after it's been aborted by KillSession is a no-op.
+ txnParticipant->abortActiveTransaction(opCtx());
+ ASSERT(txnParticipant->transactionIsAborted());
+ ASSERT(opCtx()->getWriteUnitOfWork() == nullptr);
+}
+
+TEST_F(TxnParticipantTest, ConcurrencyOfActiveAbortAndMigration) {
+ OperationContextSessionMongod opCtxSession(opCtx(), true, false, true);
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+
+ txnParticipant->unstashTransactionResources(opCtx(), "insert");
+ auto operation = repl::OplogEntry::makeInsertOperation(kNss, kUUID, BSON("TestValue" << 0));
+ txnParticipant->addTransactionOperation(opCtx(), operation);
+ ASSERT(txnParticipant->inMultiDocumentTransaction());
+
+ // A migration may bump the active transaction number without checking out the txnParticipant.
+ const auto higherTxnNum = *opCtx()->getTxnNumber() + 1;
+ bumpTxnNumberFromDifferentOpCtx(*opCtx()->getLogicalSessionId(), higherTxnNum);
+
+ // The operation throws for some reason and aborts implicitly.
+ // Abort active transaction after it's been aborted by migration is a no-op.
+ txnParticipant->abortActiveTransaction(opCtx());
+
+ // The session's state is None after migration, but we should have cleared
+ // the states of opCtx.
+ ASSERT(opCtx()->getWriteUnitOfWork() == nullptr);
+}
+
+TEST_F(TxnParticipantTest, ConcurrencyOfPrepareTransactionAndAbort) {
+ OperationContextSessionMongod opCtxSession(opCtx(), true, false, true);
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+
+ txnParticipant->unstashTransactionResources(opCtx(), "prepareTransaction");
+
+ // The transaction may be aborted without checking out the txnParticipant.
+ txnParticipant->abortArbitraryTransaction();
+ ASSERT(txnParticipant->transactionIsAborted());
+
+ // A prepareTransaction() after an abort should uassert.
+ ASSERT_THROWS_CODE(txnParticipant->prepareTransaction(opCtx()),
+ AssertionException,
+ ErrorCodes::NoSuchTransaction);
+ ASSERT_FALSE(_opObserver->transactionPrepared);
+ ASSERT(txnParticipant->transactionIsAborted());
+}
+
+TEST_F(TxnParticipantTest, ConcurrencyOfPrepareTransactionAndCommit) {
+ OperationContextSessionMongod opCtxSession(opCtx(), true, false, true);
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+
+ txnParticipant->unstashTransactionResources(opCtx(), "prepareTransaction");
+ txnParticipant->commitUnpreparedTransaction(opCtx());
+
+ // A prepareTransaction() after a commit should uassert.
+ ASSERT_THROWS_CODE(txnParticipant->prepareTransaction(opCtx()),
+ AssertionException,
+ ErrorCodes::TransactionCommitted);
+ ASSERT_FALSE(_opObserver->transactionPrepared);
+ ASSERT(txnParticipant->transactionIsCommitted());
+}
+
+TEST_F(TxnParticipantTest, KillSessionsDuringPrepareDoesNotAbortTransaction) {
+ OperationContextSessionMongod opCtxSession(opCtx(), true, false, true);
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+
+ txnParticipant->unstashTransactionResources(opCtx(), "prepareTransaction");
+
+ auto ruPrepareTimestamp = Timestamp();
+ auto originalFn = _opObserver->onTransactionPrepareFn;
+ _opObserver->onTransactionPrepareFn = [&]() {
+ originalFn();
+
+ ruPrepareTimestamp = opCtx()->recoveryUnit()->getPrepareTimestamp();
+ ASSERT_FALSE(ruPrepareTimestamp.isNull());
+
+ // The transaction may be aborted without checking out the txnParticipant.
+ txnParticipant->abortArbitraryTransaction();
+ ASSERT_FALSE(txnParticipant->transactionIsAborted());
+ };
+
+ // Check that prepareTimestamp gets set.
+ auto prepareTimestamp = txnParticipant->prepareTransaction(opCtx());
+ ASSERT_EQ(ruPrepareTimestamp, prepareTimestamp);
+ ASSERT(_opObserver->transactionPrepared);
+ ASSERT_FALSE(txnParticipant->transactionIsAborted());
+}
+
+DEATH_TEST_F(TxnParticipantTest, AbortDuringPrepareIsFatal, "Fatal assertion 50906") {
+ OperationContextSessionMongod opCtxSession(opCtx(), true, false, true);
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+ txnParticipant->unstashTransactionResources(opCtx(), "prepareTransaction");
+
+ auto originalFn = _opObserver->onTransactionPrepareFn;
+ _opObserver->onTransactionPrepareFn = [&]() {
+ originalFn();
+
+ // The transaction may be aborted without checking out the txnParticipant.
+ txnParticipant->abortActiveTransaction(opCtx());
+ ASSERT(txnParticipant->transactionIsAborted());
+ };
+
+ txnParticipant->prepareTransaction(opCtx());
+}
+
+TEST_F(TxnParticipantTest, ThrowDuringOnTransactionPrepareAbortsTransaction) {
+ OperationContextSessionMongod opCtxSession(opCtx(), true, false, true);
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+
+ txnParticipant->unstashTransactionResources(opCtx(), "prepareTransaction");
+
+ _opObserver->onTransactionPrepareThrowsException = true;
+
+ ASSERT_THROWS_CODE(txnParticipant->prepareTransaction(opCtx()),
+ AssertionException,
+ ErrorCodes::OperationFailed);
+ ASSERT_FALSE(_opObserver->transactionPrepared);
+ ASSERT(txnParticipant->transactionIsAborted());
+}
+
+TEST_F(TxnParticipantTest, KillSessionsDuringPreparedCommitDoesNotAbortTransaction) {
+ OperationContextSessionMongod opCtxSession(opCtx(), true, false, true);
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+ txnParticipant->unstashTransactionResources(opCtx(), "commitTransaction");
+
+ const auto commitTimestamp = Timestamp(1, 1);
+ auto originalFn = _opObserver->onTransactionCommitFn;
+ _opObserver->onTransactionCommitFn = [&](bool wasPrepared) {
+ originalFn(wasPrepared);
+ ASSERT(wasPrepared);
+
+ // The transaction may be aborted without checking out the txnParticipant.
+ txnParticipant->abortArbitraryTransaction();
+ ASSERT_FALSE(txnParticipant->transactionIsAborted());
+ };
+
+ txnParticipant->prepareTransaction(opCtx());
+ txnParticipant->commitPreparedTransaction(opCtx(), commitTimestamp);
+
+ ASSERT(_opObserver->transactionCommitted);
+ ASSERT_FALSE(txnParticipant->transactionIsAborted());
+ ASSERT(txnParticipant->transactionIsCommitted());
+}
+
+// This tests documents behavior, though it is not necessarily the behavior we want.
+TEST_F(TxnParticipantTest, AbortDuringPreparedCommitDoesNotAbortTransaction) {
+ OperationContextSessionMongod opCtxSession(opCtx(), true, false, true);
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+ txnParticipant->unstashTransactionResources(opCtx(), "commitTransaction");
+
+ const auto commitTimestamp = Timestamp(1, 1);
+ auto originalFn = _opObserver->onTransactionCommitFn;
+ _opObserver->onTransactionCommitFn = [&](bool wasPrepared) {
+ originalFn(wasPrepared);
+ ASSERT(wasPrepared);
+
+ // The transaction may be aborted without checking out the txnParticipant.
+ auto func = [&](OperationContext* opCtx) { txnParticipant->abortActiveTransaction(opCtx); };
+ runFunctionFromDifferentOpCtx(func);
+ ASSERT_FALSE(txnParticipant->transactionIsAborted());
+ };
+
+ txnParticipant->prepareTransaction(opCtx());
+ txnParticipant->commitPreparedTransaction(opCtx(), commitTimestamp);
+
+ ASSERT(_opObserver->transactionCommitted);
+ ASSERT_FALSE(txnParticipant->transactionIsAborted());
+ ASSERT(txnParticipant->transactionIsCommitted());
+}
+
+// This tests documents behavior, though it is not necessarily the behavior we want.
+TEST_F(TxnParticipantTest, ThrowDuringPreparedOnTransactionCommitDoesNothing) {
+ OperationContextSessionMongod opCtxSession(opCtx(), true, false, true);
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+ txnParticipant->unstashTransactionResources(opCtx(), "commitTransaction");
+
+ const auto commitTimestamp = Timestamp(1, 1);
+ _opObserver->onTransactionCommitThrowsException = true;
+ txnParticipant->prepareTransaction(opCtx());
+
+ ASSERT_THROWS_CODE(txnParticipant->commitPreparedTransaction(opCtx(), commitTimestamp),
+ AssertionException,
+ ErrorCodes::OperationFailed);
+ ASSERT_FALSE(_opObserver->transactionCommitted);
+ ASSERT_FALSE(txnParticipant->transactionIsAborted());
+ ASSERT_FALSE(txnParticipant->transactionIsCommitted());
+}
+
+TEST_F(TxnParticipantTest, KillSessionsDuringUnpreparedCommitDoesNotAbortTransaction) {
+ OperationContextSessionMongod opCtxSession(opCtx(), true, false, true);
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+ txnParticipant->unstashTransactionResources(opCtx(), "commitTransaction");
+
+ auto originalFn = _opObserver->onTransactionCommitFn;
+ _opObserver->onTransactionCommitFn = [&](bool wasPrepared) {
+ originalFn(wasPrepared);
+ ASSERT_FALSE(wasPrepared);
+
+ // The transaction may be aborted without checking out the txnParticipant.
+ txnParticipant->abortArbitraryTransaction();
+ ASSERT_FALSE(txnParticipant->transactionIsAborted());
+ };
+
+ txnParticipant->commitUnpreparedTransaction(opCtx());
+
+ ASSERT(_opObserver->transactionCommitted);
+ ASSERT_FALSE(txnParticipant->transactionIsAborted());
+ ASSERT(txnParticipant->transactionIsCommitted());
+}
+
+TEST_F(TxnParticipantTest, AbortDuringUnpreparedCommitDoesNotAbortTransaction) {
+ OperationContextSessionMongod opCtxSession(opCtx(), true, false, true);
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+ txnParticipant->unstashTransactionResources(opCtx(), "commitTransaction");
+
+ auto originalFn = _opObserver->onTransactionCommitFn;
+ _opObserver->onTransactionCommitFn = [&](bool wasPrepared) {
+ originalFn(wasPrepared);
+ ASSERT_FALSE(wasPrepared);
+
+ // The transaction may be aborted without checking out the txnParticipant.
+ auto func = [&](OperationContext* opCtx) { txnParticipant->abortActiveTransaction(opCtx); };
+ runFunctionFromDifferentOpCtx(func);
+ ASSERT_FALSE(txnParticipant->transactionIsAborted());
+ };
+
+ txnParticipant->commitUnpreparedTransaction(opCtx());
+
+ ASSERT(_opObserver->transactionCommitted);
+ ASSERT_FALSE(txnParticipant->transactionIsAborted());
+ ASSERT(txnParticipant->transactionIsCommitted());
+}
+
+// This tests documents behavior, though it is not necessarily the behavior we want.
+TEST_F(TxnParticipantTest, ThrowDuringUnpreparedOnTransactionCommitDoesNothing) {
+ OperationContextSessionMongod opCtxSession(opCtx(), true, false, true);
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+ txnParticipant->unstashTransactionResources(opCtx(), "commitTransaction");
+
+ _opObserver->onTransactionCommitThrowsException = true;
+
+ ASSERT_THROWS_CODE(txnParticipant->commitUnpreparedTransaction(opCtx()),
+ AssertionException,
+ ErrorCodes::OperationFailed);
+ ASSERT_FALSE(_opObserver->transactionCommitted);
+ ASSERT_FALSE(txnParticipant->transactionIsAborted());
+ ASSERT_FALSE(txnParticipant->transactionIsCommitted());
+}
+
+TEST_F(TxnParticipantTest, ConcurrencyOfCommitTransactionAndMigration) {
+ OperationContextSessionMongod opCtxSession(opCtx(), true, false, true);
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+
+ txnParticipant->unstashTransactionResources(opCtx(), "insert");
+ auto operation = repl::OplogEntry::makeInsertOperation(kNss, kUUID, BSON("TestValue" << 0));
+ txnParticipant->addTransactionOperation(opCtx(), operation);
+
+ // A migration may bump the active transaction number without checking out the txnParticipant.
+ const auto higherTxnNum = *opCtx()->getTxnNumber() + 1;
+ bumpTxnNumberFromDifferentOpCtx(*opCtx()->getLogicalSessionId(), higherTxnNum);
+
+ // An commitPreparedTransaction() after a migration that bumps the active transaction number
+ // should uassert.
+ ASSERT_THROWS_CODE(txnParticipant->commitUnpreparedTransaction(opCtx()),
+ AssertionException,
+ ErrorCodes::ConflictingOperationInProgress);
+}
+
+TEST_F(TxnParticipantTest, ConcurrencyOfPrepareTransactionAndMigration) {
+ OperationContextSessionMongod opCtxSession(opCtx(), true, false, true);
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+
+ txnParticipant->unstashTransactionResources(opCtx(), "insert");
+ auto operation = repl::OplogEntry::makeInsertOperation(kNss, kUUID, BSON("TestValue" << 0));
+ txnParticipant->addTransactionOperation(opCtx(), operation);
+
+ // A migration may bump the active transaction number without checking out the txnParticipant.
+ const auto higherTxnNum = *opCtx()->getTxnNumber() + 1;
+ bumpTxnNumberFromDifferentOpCtx(*opCtx()->getLogicalSessionId(), higherTxnNum);
+
+ // A prepareTransaction() after a migration that bumps the active transaction number should
+ // uassert.
+ ASSERT_THROWS_CODE(txnParticipant->prepareTransaction(opCtx()),
+ AssertionException,
+ ErrorCodes::ConflictingOperationInProgress);
+ ASSERT_FALSE(_opObserver->transactionPrepared);
+}
+
+TEST_F(TxnParticipantTest, ContinuingATransactionWithNoResourcesAborts) {
+ ASSERT_THROWS_CODE(OperationContextSessionMongod(opCtx(), true, false, boost::none),
+ AssertionException,
+ ErrorCodes::NoSuchTransaction);
+}
+
+TEST_F(TxnParticipantTest, KillSessionsDoesNotAbortPreparedTransactions) {
+ OperationContextSessionMongod opCtxSession(opCtx(), true, false, true);
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+
+ txnParticipant->unstashTransactionResources(opCtx(), "insert");
+
+ auto ruPrepareTimestamp = Timestamp();
+ auto originalFn = _opObserver->onTransactionPrepareFn;
+ _opObserver->onTransactionPrepareFn = [&]() {
+ originalFn();
+ ruPrepareTimestamp = opCtx()->recoveryUnit()->getPrepareTimestamp();
+ ASSERT_FALSE(ruPrepareTimestamp.isNull());
+ };
+
+ // Check that prepareTimestamp gets set.
+ auto prepareTimestamp = txnParticipant->prepareTransaction(opCtx());
+ ASSERT_EQ(ruPrepareTimestamp, prepareTimestamp);
+ txnParticipant->stashTransactionResources(opCtx());
+
+ txnParticipant->abortArbitraryTransaction();
+ ASSERT_FALSE(txnParticipant->transactionIsAborted());
+ ASSERT(_opObserver->transactionPrepared);
+}
+
+TEST_F(TxnParticipantTest, CannotStartNewTransactionWhilePreparedTransactionInProgress) {
+ OperationContextSessionMongod opCtxSession(opCtx(), true, false, true);
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+
+ txnParticipant->unstashTransactionResources(opCtx(), "insert");
+
+ auto ruPrepareTimestamp = Timestamp();
+ auto originalFn = _opObserver->onTransactionPrepareFn;
+ _opObserver->onTransactionPrepareFn = [&]() {
+ originalFn();
+
+ ruPrepareTimestamp = opCtx()->recoveryUnit()->getPrepareTimestamp();
+ ASSERT_FALSE(ruPrepareTimestamp.isNull());
+ };
+
+ // Check that prepareTimestamp gets set.
+ auto prepareTimestamp = txnParticipant->prepareTransaction(opCtx());
+ ASSERT_EQ(ruPrepareTimestamp, prepareTimestamp);
+
+ txnParticipant->stashTransactionResources(opCtx());
+
+ {
+ // Try to start a new transaction while there is already a prepared transaction on the
+ // session. This should fail with a PreparedTransactionInProgress error.
+
+ auto func = [&](OperationContext* newOpCtx) {
+ // newOpCtx->setLogicalSessionId(*opCtx()->getLogicalSessionId());
+ // newOpCtx->setTxnNumber(*opCtx()->getTxnNumber() + 1);
+
+ auto session = SessionCatalog::get(newOpCtx)->getOrCreateSession(
+ newOpCtx, *opCtx()->getLogicalSessionId());
+
+ ASSERT_THROWS_CODE(
+ session->onMigrateBeginOnPrimary(newOpCtx, *opCtx()->getTxnNumber() + 1, 1),
+ AssertionException,
+ ErrorCodes::PreparedTransactionInProgress);
+ };
+
+ runFunctionFromDifferentOpCtx(func);
+ }
+
+ ASSERT_FALSE(txnParticipant->transactionIsAborted());
+ ASSERT(_opObserver->transactionPrepared);
+}
+
+// Tests that a transaction aborts if it becomes too large before trying to commit it.
+TEST_F(TxnParticipantTest, TransactionTooLargeWhileBuilding) {
+ OperationContextSessionMongod opCtxSession(opCtx(), true, false, true);
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+
+ txnParticipant->unstashTransactionResources(opCtx(), "insert");
+
+ // Two 6MB operations should succeed; three 6MB operations should fail.
+ constexpr size_t kBigDataSize = 6 * 1024 * 1024;
+ std::unique_ptr<uint8_t[]> bigData(new uint8_t[kBigDataSize]());
+ auto operation = repl::OplogEntry::makeInsertOperation(
+ kNss,
+ kUUID,
+ BSON("_id" << 0 << "data" << BSONBinData(bigData.get(), kBigDataSize, BinDataGeneral)));
+ txnParticipant->addTransactionOperation(opCtx(), operation);
+ txnParticipant->addTransactionOperation(opCtx(), operation);
+ ASSERT_THROWS_CODE(txnParticipant->addTransactionOperation(opCtx(), operation),
+ AssertionException,
+ ErrorCodes::TransactionTooLarge);
+}
+
+TEST_F(TxnParticipantTest, IncrementTotalStartedUponStartTransaction) {
+ unsigned long long beforeTransactionStart =
+ ServerTransactionsMetrics::get(opCtx())->getTotalStarted();
+
+ OperationContextSessionMongod opCtxSession(opCtx(), true, false, true);
+
+ // Tests that the total transactions started counter is incremented by 1 when a new transaction
+ // is started.
+ ASSERT_EQ(ServerTransactionsMetrics::get(opCtx())->getTotalStarted(),
+ beforeTransactionStart + 1U);
+}
+
+TEST_F(TxnParticipantTest, IncrementTotalCommittedOnCommit) {
+ OperationContextSessionMongod opCtxSession(opCtx(), true, false, true);
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+ txnParticipant->unstashTransactionResources(opCtx(), "commitTransaction");
+
+ unsigned long long beforeCommitCount =
+ ServerTransactionsMetrics::get(opCtx())->getTotalCommitted();
+
+ txnParticipant->commitUnpreparedTransaction(opCtx());
+
+ // Assert that the committed counter is incremented by 1.
+ ASSERT_EQ(ServerTransactionsMetrics::get(opCtx())->getTotalCommitted(), beforeCommitCount + 1U);
+}
+
+TEST_F(TxnParticipantTest, IncrementTotalAbortedUponAbort) {
+ OperationContextSessionMongod opCtxSession(opCtx(), true, false, true);
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+ txnParticipant->unstashTransactionResources(opCtx(), "insert");
+
+ unsigned long long beforeAbortCount =
+ ServerTransactionsMetrics::get(opCtx())->getTotalAborted();
+
+ txnParticipant->abortArbitraryTransaction();
+
+ // Assert that the aborted counter is incremented by 1.
+ ASSERT_EQ(ServerTransactionsMetrics::get(opCtx())->getTotalAborted(), beforeAbortCount + 1U);
+}
+
+TEST_F(TxnParticipantTest, TrackTotalOpenTransactionsWithAbort) {
+ unsigned long long beforeTransactionStart =
+ ServerTransactionsMetrics::get(opCtx())->getCurrentOpen();
+
+ // Tests that starting a transaction increments the open transactions counter by 1.
+ OperationContextSessionMongod opCtxSession(opCtx(), true, false, true);
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+ txnParticipant->unstashTransactionResources(opCtx(), "insert");
+ ASSERT_EQ(ServerTransactionsMetrics::get(opCtx())->getCurrentOpen(),
+ beforeTransactionStart + 1U);
+
+ // Tests that stashing the transaction resources does not affect the open transactions counter.
+ { Lock::GlobalLock lk(opCtx(), MODE_IX, Date_t::now(), Lock::InterruptBehavior::kThrow); }
+ txnParticipant->stashTransactionResources(opCtx());
+ ASSERT_EQ(ServerTransactionsMetrics::get(opCtx())->getCurrentOpen(),
+ beforeTransactionStart + 1U);
+
+ // Tests that aborting a transaction decrements the open transactions counter by 1.
+ txnParticipant->abortArbitraryTransaction();
+ ASSERT_EQ(ServerTransactionsMetrics::get(opCtx())->getCurrentOpen(), beforeTransactionStart);
+}
+
+TEST_F(TxnParticipantTest, TrackTotalOpenTransactionsWithCommit) {
+ unsigned long long beforeTransactionStart =
+ ServerTransactionsMetrics::get(opCtx())->getCurrentOpen();
+
+ // Tests that starting a transaction increments the open transactions counter by 1.
+ OperationContextSessionMongod opCtxSession(opCtx(), true, false, true);
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+ txnParticipant->unstashTransactionResources(opCtx(), "insert");
+ ASSERT_EQ(ServerTransactionsMetrics::get(opCtx())->getCurrentOpen(),
+ beforeTransactionStart + 1U);
+
+ // Tests that stashing the transaction resources does not affect the open transactions counter.
+ { Lock::GlobalLock lk(opCtx(), MODE_IX, Date_t::now(), Lock::InterruptBehavior::kThrow); }
+ txnParticipant->stashTransactionResources(opCtx());
+ ASSERT_EQ(ServerTransactionsMetrics::get(opCtx())->getCurrentOpen(),
+ beforeTransactionStart + 1U);
+
+ txnParticipant->unstashTransactionResources(opCtx(), "insert");
+
+ // Tests that committing a transaction decrements the open transactions counter by 1.
+ txnParticipant->commitUnpreparedTransaction(opCtx());
+ ASSERT_EQ(ServerTransactionsMetrics::get(opCtx())->getCurrentOpen(), beforeTransactionStart);
+}
+
+TEST_F(TxnParticipantTest, TrackTotalActiveAndInactiveTransactionsWithCommit) {
+ unsigned long long beforeActiveCounter =
+ ServerTransactionsMetrics::get(opCtx())->getCurrentActive();
+ unsigned long long beforeInactiveCounter =
+ ServerTransactionsMetrics::get(opCtx())->getCurrentInactive();
+
+ // Tests that the first unstash only increments the active counter only.
+ OperationContextSessionMongod opCtxSession(opCtx(), true, false, true);
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+ txnParticipant->unstashTransactionResources(opCtx(), "insert");
+ ASSERT_EQ(ServerTransactionsMetrics::get(opCtx())->getCurrentActive(),
+ beforeActiveCounter + 1U);
+ ASSERT_EQ(ServerTransactionsMetrics::get(opCtx())->getCurrentInactive(), beforeInactiveCounter);
+
+ // Tests that stashing the transaction resources decrements active counter and increments
+ // inactive counter.
+ { Lock::GlobalLock lk(opCtx(), MODE_IX, Date_t::now(), Lock::InterruptBehavior::kThrow); }
+ txnParticipant->stashTransactionResources(opCtx());
+ ASSERT_EQ(ServerTransactionsMetrics::get(opCtx())->getCurrentActive(), beforeActiveCounter);
+ ASSERT_EQ(ServerTransactionsMetrics::get(opCtx())->getCurrentInactive(),
+ beforeInactiveCounter + 1U);
+
+ // Tests that the second unstash increments the active counter and decrements the inactive
+ // counter.
+ txnParticipant->unstashTransactionResources(opCtx(), "insert");
+ ASSERT_EQ(ServerTransactionsMetrics::get(opCtx())->getCurrentActive(),
+ beforeActiveCounter + 1U);
+ ASSERT_EQ(ServerTransactionsMetrics::get(opCtx())->getCurrentInactive(), beforeInactiveCounter);
+
+ // Tests that committing a transaction decrements the active counter only.
+ txnParticipant->commitUnpreparedTransaction(opCtx());
+ ASSERT_EQ(ServerTransactionsMetrics::get(opCtx())->getCurrentActive(), beforeActiveCounter);
+ ASSERT_EQ(ServerTransactionsMetrics::get(opCtx())->getCurrentInactive(), beforeInactiveCounter);
+}
+
+TEST_F(TxnParticipantTest, TrackTotalActiveAndInactiveTransactionsWithStashedAbort) {
+ unsigned long long beforeActiveCounter =
+ ServerTransactionsMetrics::get(opCtx())->getCurrentActive();
+ unsigned long long beforeInactiveCounter =
+ ServerTransactionsMetrics::get(opCtx())->getCurrentInactive();
+
+ OperationContextSessionMongod opCtxSession(opCtx(), true, false, true);
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+
+ // Tests that the first unstash only increments the active counter only.
+ txnParticipant->unstashTransactionResources(opCtx(), "insert");
+ ASSERT_EQ(ServerTransactionsMetrics::get(opCtx())->getCurrentActive(),
+ beforeActiveCounter + 1U);
+ ASSERT_EQ(ServerTransactionsMetrics::get(opCtx())->getCurrentInactive(), beforeInactiveCounter);
+
+ // Tests that stashing the transaction resources decrements active counter and increments
+ // inactive counter.
+ { Lock::GlobalLock lk(opCtx(), MODE_IX, Date_t::now(), Lock::InterruptBehavior::kThrow); }
+ txnParticipant->stashTransactionResources(opCtx());
+ ASSERT_EQ(ServerTransactionsMetrics::get(opCtx())->getCurrentActive(), beforeActiveCounter);
+ ASSERT_EQ(ServerTransactionsMetrics::get(opCtx())->getCurrentInactive(),
+ beforeInactiveCounter + 1U);
+
+ // Tests that aborting a stashed transaction decrements the inactive counter only.
+ txnParticipant->abortArbitraryTransaction();
+ ASSERT_EQ(ServerTransactionsMetrics::get(opCtx())->getCurrentActive(), beforeActiveCounter);
+ ASSERT_EQ(ServerTransactionsMetrics::get(opCtx())->getCurrentInactive(), beforeInactiveCounter);
+}
+
+TEST_F(TxnParticipantTest, TrackTotalActiveAndInactiveTransactionsWithUnstashedAbort) {
+ unsigned long long beforeActiveCounter =
+ ServerTransactionsMetrics::get(opCtx())->getCurrentActive();
+ unsigned long long beforeInactiveCounter =
+ ServerTransactionsMetrics::get(opCtx())->getCurrentInactive();
+
+ OperationContextSessionMongod opCtxSession(opCtx(), true, false, true);
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+
+ // Tests that the first unstash only increments the active counter only.
+ txnParticipant->unstashTransactionResources(opCtx(), "insert");
+ ASSERT_EQ(ServerTransactionsMetrics::get(opCtx())->getCurrentActive(),
+ beforeActiveCounter + 1U);
+ ASSERT_EQ(ServerTransactionsMetrics::get(opCtx())->getCurrentInactive(), beforeInactiveCounter);
+
+ // Tests that aborting a stashed transaction decrements the active counter only.
+ txnParticipant->abortArbitraryTransaction();
+ ASSERT_EQ(ServerTransactionsMetrics::get(opCtx())->getCurrentActive(), beforeActiveCounter);
+ ASSERT_EQ(ServerTransactionsMetrics::get(opCtx())->getCurrentInactive(), beforeInactiveCounter);
+}
+
+TEST_F(TxnParticipantTest, StashInNestedSessionIsANoop) {
+ OperationContextSessionMongod outerScopedSession(opCtx(), true, false, true);
+ Locker* originalLocker = opCtx()->lockState();
+ RecoveryUnit* originalRecoveryUnit = opCtx()->recoveryUnit();
+ ASSERT(originalLocker);
+ ASSERT(originalRecoveryUnit);
+
+ // Set the readConcern on the OperationContext.
+ repl::ReadConcernArgs readConcernArgs;
+ ASSERT_OK(readConcernArgs.initialize(BSON("find"
+ << "test"
+ << repl::ReadConcernArgs::kReadConcernFieldName
+ << BSON(repl::ReadConcernArgs::kLevelFieldName
+ << "snapshot"))));
+ repl::ReadConcernArgs::get(opCtx()) = readConcernArgs;
+
+ // Perform initial unstash, which sets up a WriteUnitOfWork.
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+ txnParticipant->unstashTransactionResources(opCtx(), "find");
+ ASSERT_EQUALS(originalLocker, opCtx()->lockState());
+ ASSERT_EQUALS(originalRecoveryUnit, opCtx()->recoveryUnit());
+ ASSERT(opCtx()->getWriteUnitOfWork());
+
+ {
+ // Make it look like we're in a DBDirectClient running a nested operation.
+ DirectClientSetter inDirectClient(opCtx());
+ OperationContextSessionMongod innerScopedSession(opCtx(), true, boost::none, boost::none);
+
+ txnParticipant->stashTransactionResources(opCtx());
+
+ // The stash was a noop, so the locker, RecoveryUnit, and WriteUnitOfWork on the
+ // OperationContext are unaffected.
+ ASSERT_EQUALS(originalLocker, opCtx()->lockState());
+ ASSERT_EQUALS(originalRecoveryUnit, opCtx()->recoveryUnit());
+ ASSERT(opCtx()->getWriteUnitOfWork());
+ }
+}
+
+TEST_F(TxnParticipantTest, UnstashInNestedSessionIsANoop) {
+
+ OperationContextSessionMongod outerScopedSession(opCtx(), true, false, true);
+
+ Locker* originalLocker = opCtx()->lockState();
+ RecoveryUnit* originalRecoveryUnit = opCtx()->recoveryUnit();
+ ASSERT(originalLocker);
+ ASSERT(originalRecoveryUnit);
+
+ // Set the readConcern on the OperationContext.
+ repl::ReadConcernArgs readConcernArgs;
+ ASSERT_OK(readConcernArgs.initialize(BSON("find"
+ << "test"
+ << repl::ReadConcernArgs::kReadConcernFieldName
+ << BSON(repl::ReadConcernArgs::kLevelFieldName
+ << "snapshot"))));
+ repl::ReadConcernArgs::get(opCtx()) = readConcernArgs;
+
+ {
+ // Make it look like we're in a DBDirectClient running a nested operation.
+ DirectClientSetter inDirectClient(opCtx());
+ OperationContextSessionMongod innerScopedSession(opCtx(), true, boost::none, boost::none);
+
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+ txnParticipant->unstashTransactionResources(opCtx(), "find");
+
+ // The unstash was a noop, so the OperationContext did not get a WriteUnitOfWork.
+ ASSERT_EQUALS(originalLocker, opCtx()->lockState());
+ ASSERT_EQUALS(originalRecoveryUnit, opCtx()->recoveryUnit());
+ ASSERT_FALSE(opCtx()->getWriteUnitOfWork());
+ }
+}
+
+/**
+ * Test fixture for transactions metrics.
+ */
+class TransactionsMetricsTest : public TxnParticipantTest {};
+
+TEST_F(TransactionsMetricsTest, SingleTransactionStatsStartTimeShouldBeSetUponTransactionStart) {
+ // Save the time before the transaction is created.
+ unsigned long long timeBeforeTxn = curTimeMicros64();
+ OperationContextSessionMongod opCtxSession(opCtx(), true, false, true);
+
+ unsigned long long timeAfterTxn = curTimeMicros64();
+
+ // Start time should be greater than or equal to the time before the transaction was created.
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+ ASSERT_GTE(txnParticipant->getSingleTransactionStats().getStartTime(), timeBeforeTxn);
+
+ // Start time should be less than or equal to the time after the transaction was started.
+ ASSERT_LTE(txnParticipant->getSingleTransactionStats().getStartTime(), timeAfterTxn);
+}
+
+TEST_F(TransactionsMetricsTest, SingleTransactionStatsDurationShouldBeSetUponCommit) {
+ unsigned long long timeBeforeTxnStart = curTimeMicros64();
+ OperationContextSessionMongod opCtxSession(opCtx(), true, false, true);
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+ unsigned long long timeAfterTxnStart = curTimeMicros64();
+ txnParticipant->unstashTransactionResources(opCtx(), "commitTransaction");
+ // The transaction machinery cannot store an empty locker.
+ Lock::GlobalLock lk(opCtx(), MODE_IX, Date_t::now(), Lock::InterruptBehavior::kThrow);
+
+ // Sleep here to allow enough time to elapse.
+ sleepmillis(10);
+
+ unsigned long long timeBeforeTxnCommit = curTimeMicros64();
+ txnParticipant->commitUnpreparedTransaction(opCtx());
+ unsigned long long timeAfterTxnCommit = curTimeMicros64();
+
+ ASSERT_GTE(txnParticipant->getSingleTransactionStats().getDuration(curTimeMicros64()),
+ timeBeforeTxnCommit - timeAfterTxnStart);
+
+ ASSERT_LTE(txnParticipant->getSingleTransactionStats().getDuration(curTimeMicros64()),
+ timeAfterTxnCommit - timeBeforeTxnStart);
+}
+
+TEST_F(TransactionsMetricsTest, SingleTransactionStatsDurationShouldBeSetUponAbort) {
+ unsigned long long timeBeforeTxnStart = curTimeMicros64();
+ OperationContextSessionMongod opCtxSession(opCtx(), true, false, true);
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+ unsigned long long timeAfterTxnStart = curTimeMicros64();
+ txnParticipant->unstashTransactionResources(opCtx(), "insert");
+
+ // Sleep here to allow enough time to elapse.
+ sleepmillis(10);
+
+ unsigned long long timeBeforeTxnAbort = curTimeMicros64();
+ txnParticipant->abortArbitraryTransaction();
+ unsigned long long timeAfterTxnAbort = curTimeMicros64();
+
+ ASSERT_GTE(txnParticipant->getSingleTransactionStats().getDuration(curTimeMicros64()),
+ timeBeforeTxnAbort - timeAfterTxnStart);
+
+ ASSERT_LTE(txnParticipant->getSingleTransactionStats().getDuration(curTimeMicros64()),
+ timeAfterTxnAbort - timeBeforeTxnStart);
+}
+
+TEST_F(TransactionsMetricsTest, SingleTransactionStatsDurationShouldKeepIncreasingUntilCommit) {
+ OperationContextSessionMongod opCtxSession(opCtx(), true, false, true);
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+ txnParticipant->unstashTransactionResources(opCtx(), "commitTransaction");
+ // The transaction machinery cannot store an empty locker.
+ Lock::GlobalLock lk(opCtx(), MODE_IX, Date_t::now(), Lock::InterruptBehavior::kThrow);
+
+ // Save the transaction's duration at this point.
+ unsigned long long txnDurationAfterStart =
+ txnParticipant->getSingleTransactionStats().getDuration(curTimeMicros64());
+ sleepmillis(10);
+
+ // The transaction's duration should have increased.
+ ASSERT_GT(txnParticipant->getSingleTransactionStats().getDuration(curTimeMicros64()),
+ txnDurationAfterStart);
+ sleepmillis(10);
+ txnParticipant->commitUnpreparedTransaction(opCtx());
+ unsigned long long txnDurationAfterCommit =
+ txnParticipant->getSingleTransactionStats().getDuration(curTimeMicros64());
+
+ // The transaction has committed, so the duration should have not increased.
+ ASSERT_EQ(txnParticipant->getSingleTransactionStats().getDuration(curTimeMicros64()),
+ txnDurationAfterCommit);
+
+ ASSERT_GT(txnDurationAfterCommit, txnDurationAfterStart);
+}
+
+TEST_F(TransactionsMetricsTest, SingleTransactionStatsDurationShouldKeepIncreasingUntilAbort) {
+ OperationContextSessionMongod opCtxSession(opCtx(), true, false, true);
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+ txnParticipant->unstashTransactionResources(opCtx(), "insert");
+ // The transaction machinery cannot store an empty locker.
+ Lock::GlobalLock lk(opCtx(), MODE_IX, Date_t::now(), Lock::InterruptBehavior::kThrow);
+
+ // Save the transaction's duration at this point.
+ unsigned long long txnDurationAfterStart =
+ txnParticipant->getSingleTransactionStats().getDuration(curTimeMicros64());
+ sleepmillis(10);
+
+ // The transaction's duration should have increased.
+ ASSERT_GT(txnParticipant->getSingleTransactionStats().getDuration(curTimeMicros64()),
+ txnDurationAfterStart);
+ sleepmillis(10);
+ txnParticipant->abortArbitraryTransaction();
+ unsigned long long txnDurationAfterAbort =
+ txnParticipant->getSingleTransactionStats().getDuration(curTimeMicros64());
+
+ // The transaction has aborted, so the duration should have not increased.
+ ASSERT_EQ(txnParticipant->getSingleTransactionStats().getDuration(curTimeMicros64()),
+ txnDurationAfterAbort);
+
+ ASSERT_GT(txnDurationAfterAbort, txnDurationAfterStart);
+}
+
+TEST_F(TransactionsMetricsTest, TimeActiveMicrosShouldBeSetUponUnstashAndStash) {
+ OperationContextSessionMongod opCtxSession(opCtx(), true, false, true);
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+
+ // Time active should be zero.
+ ASSERT_EQ(txnParticipant->getSingleTransactionStats().getTimeActiveMicros(curTimeMicros64()),
+ Microseconds{0});
+
+ txnParticipant->unstashTransactionResources(opCtx(), "insert");
+ // The transaction machinery cannot store an empty locker.
+ { Lock::GlobalLock lk(opCtx(), MODE_IX, Date_t::now(), Lock::InterruptBehavior::kThrow); }
+ txnParticipant->stashTransactionResources(opCtx());
+
+ // Time active should have increased.
+ ASSERT_GT(txnParticipant->getSingleTransactionStats().getTimeActiveMicros(curTimeMicros64()),
+ Microseconds{0});
+
+ // Save time active at this point.
+ auto timeActiveSoFar =
+ txnParticipant->getSingleTransactionStats().getTimeActiveMicros(curTimeMicros64());
+
+ txnParticipant->unstashTransactionResources(opCtx(), "insert");
+ // Sleep here to allow enough time to elapse.
+ sleepmillis(10);
+ txnParticipant->stashTransactionResources(opCtx());
+
+ // Time active should have increased again.
+ ASSERT_GT(txnParticipant->getSingleTransactionStats().getTimeActiveMicros(curTimeMicros64()),
+ timeActiveSoFar);
+
+ // Start a new transaction.
+ const auto higherTxnNum = *opCtx()->getTxnNumber() + 1;
+ txnParticipant->beginOrContinue(higherTxnNum, false, true);
+
+ // Time active should be zero for a new transaction.
+ ASSERT_EQ(txnParticipant->getSingleTransactionStats().getTimeActiveMicros(curTimeMicros64()),
+ Microseconds{0});
+}
+
+TEST_F(TransactionsMetricsTest, TimeActiveMicrosShouldBeSetUponUnstashAndAbort) {
+ OperationContextSessionMongod opCtxSession(opCtx(), true, false, true);
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+
+ // Time active should be zero.
+ ASSERT_EQ(txnParticipant->getSingleTransactionStats().getTimeActiveMicros(curTimeMicros64()),
+ Microseconds{0});
+
+ txnParticipant->unstashTransactionResources(opCtx(), "insert");
+ // Sleep here to allow enough time to elapse.
+ sleepmillis(10);
+ txnParticipant->abortArbitraryTransaction();
+
+ // Time active should have increased.
+ ASSERT_GT(txnParticipant->getSingleTransactionStats().getTimeActiveMicros(curTimeMicros64()),
+ Microseconds{0});
+
+ // Save time active at this point.
+ auto timeActiveSoFar =
+ txnParticipant->getSingleTransactionStats().getTimeActiveMicros(curTimeMicros64());
+
+ // The transaction is no longer active, so time active should not have increased.
+ ASSERT_EQ(txnParticipant->getSingleTransactionStats().getTimeActiveMicros(curTimeMicros64()),
+ timeActiveSoFar);
+}
+
+TEST_F(TransactionsMetricsTest, TimeActiveMicrosShouldNotBeSetUponAbortOnly) {
+ OperationContextSessionMongod opCtxSession(opCtx(), true, false, true);
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+
+ // Time active should be zero.
+ ASSERT_EQ(txnParticipant->getSingleTransactionStats().getTimeActiveMicros(curTimeMicros64()),
+ Microseconds{0});
+
+ txnParticipant->abortArbitraryTransaction();
+
+ // Time active should not have increased.
+ ASSERT_EQ(txnParticipant->getSingleTransactionStats().getTimeActiveMicros(curTimeMicros64()),
+ Microseconds{0});
+}
+
+TEST_F(TransactionsMetricsTest, TimeActiveMicrosShouldIncreaseUntilStash) {
+ OperationContextSessionMongod opCtxSession(opCtx(), true, false, true);
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+
+ // Time active should be zero.
+ ASSERT_EQ(txnParticipant->getSingleTransactionStats().getTimeActiveMicros(curTimeMicros64()),
+ Microseconds{0});
+ txnParticipant->unstashTransactionResources(opCtx(), "insert");
+ sleepmillis(1);
+
+ // Time active should have increased.
+ ASSERT_GT(txnParticipant->getSingleTransactionStats().getTimeActiveMicros(curTimeMicros64()),
+ Microseconds{0});
+
+ // Save time active at this point.
+ auto timeActiveSoFar =
+ txnParticipant->getSingleTransactionStats().getTimeActiveMicros(curTimeMicros64());
+ sleepmillis(1);
+
+ // Time active should have increased again.
+ ASSERT_GT(txnParticipant->getSingleTransactionStats().getTimeActiveMicros(curTimeMicros64()),
+ timeActiveSoFar);
+ // The transaction machinery cannot store an empty locker.
+ { Lock::GlobalLock lk(opCtx(), MODE_IX, Date_t::now(), Lock::InterruptBehavior::kThrow); }
+ txnParticipant->stashTransactionResources(opCtx());
+
+ // The transaction is no longer active, so time active should not have increased.
+ timeActiveSoFar =
+ txnParticipant->getSingleTransactionStats().getTimeActiveMicros(curTimeMicros64());
+ sleepmillis(1);
+ ASSERT_EQ(txnParticipant->getSingleTransactionStats().getTimeActiveMicros(curTimeMicros64()),
+ timeActiveSoFar);
+}
+
+TEST_F(TransactionsMetricsTest, TimeActiveMicrosShouldIncreaseUntilCommit) {
+ OperationContextSessionMongod opCtxSession(opCtx(), true, false, true);
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+
+ // Time active should be zero.
+ ASSERT_EQ(txnParticipant->getSingleTransactionStats().getTimeActiveMicros(curTimeMicros64()),
+ Microseconds{0});
+ txnParticipant->unstashTransactionResources(opCtx(), "commitTransaction");
+ sleepmillis(1);
+
+ // Time active should have increased.
+ ASSERT_GT(txnParticipant->getSingleTransactionStats().getTimeActiveMicros(curTimeMicros64()),
+ Microseconds{0});
+
+ // Save time active at this point.
+ auto timeActiveSoFar =
+ txnParticipant->getSingleTransactionStats().getTimeActiveMicros(curTimeMicros64());
+ sleepmillis(1);
+
+ // Time active should have increased again.
+ ASSERT_GT(txnParticipant->getSingleTransactionStats().getTimeActiveMicros(curTimeMicros64()),
+ timeActiveSoFar);
+ txnParticipant->commitUnpreparedTransaction(opCtx());
+
+ // The transaction is no longer active, so time active should not have increased.
+ timeActiveSoFar =
+ txnParticipant->getSingleTransactionStats().getTimeActiveMicros(curTimeMicros64());
+ sleepmillis(1);
+ ASSERT_EQ(txnParticipant->getSingleTransactionStats().getTimeActiveMicros(curTimeMicros64()),
+ timeActiveSoFar);
+}
+
+TEST_F(TransactionsMetricsTest, TimeActiveMicrosShouldNotBeSetIfUnstashHasBadReadConcernArgs) {
+ OperationContextSessionMongod opCtxSession(opCtx(), true, false, true);
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+
+ // Initialize bad read concern args (!readConcernArgs.isEmpty()).
+ repl::ReadConcernArgs readConcernArgs(repl::ReadConcernLevel::kLocalReadConcern);
+ repl::ReadConcernArgs::get(opCtx()) = readConcernArgs;
+
+ // Transaction resources do not exist yet.
+ txnParticipant->unstashTransactionResources(opCtx(), "find");
+ // The transaction machinery cannot store an empty locker.
+ { Lock::GlobalLock lk(opCtx(), MODE_IX, Date_t::now(), Lock::InterruptBehavior::kThrow); }
+ txnParticipant->stashTransactionResources(opCtx());
+
+ // Time active should have increased.
+ ASSERT_GT(txnParticipant->getSingleTransactionStats().getTimeActiveMicros(curTimeMicros64()),
+ Microseconds{0});
+
+ // Save time active at this point.
+ auto timeActiveSoFar =
+ txnParticipant->getSingleTransactionStats().getTimeActiveMicros(curTimeMicros64());
+
+ // Transaction resources already exist here and should throw an exception due to bad read
+ // concern arguments.
+ ASSERT_THROWS_CODE(txnParticipant->unstashTransactionResources(opCtx(), "find"),
+ AssertionException,
+ ErrorCodes::InvalidOptions);
+
+ // Time active should not have increased.
+ ASSERT_EQ(txnParticipant->getSingleTransactionStats().getTimeActiveMicros(curTimeMicros64()),
+ timeActiveSoFar);
+}
+
+TEST_F(TransactionsMetricsTest, AdditiveMetricsObjectsShouldBeAddedTogetherUponStash) {
+ OperationContextSessionMongod opCtxSession(opCtx(), true, false, true);
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+
+ // Initialize field values for both AdditiveMetrics objects.
+ txnParticipant->getSingleTransactionStats().getOpDebug()->additiveMetrics.keysExamined = 1;
+ CurOp::get(opCtx())->debug().additiveMetrics.keysExamined = 5;
+ txnParticipant->getSingleTransactionStats().getOpDebug()->additiveMetrics.docsExamined = 2;
+ CurOp::get(opCtx())->debug().additiveMetrics.docsExamined = 0;
+ txnParticipant->getSingleTransactionStats().getOpDebug()->additiveMetrics.nMatched = 3;
+ txnParticipant->getSingleTransactionStats().getOpDebug()->additiveMetrics.nModified = 1;
+ CurOp::get(opCtx())->debug().additiveMetrics.nModified = 1;
+ CurOp::get(opCtx())->debug().additiveMetrics.ninserted = 4;
+ txnParticipant->getSingleTransactionStats().getOpDebug()->additiveMetrics.nmoved = 3;
+ CurOp::get(opCtx())->debug().additiveMetrics.nmoved = 2;
+ txnParticipant->getSingleTransactionStats().getOpDebug()->additiveMetrics.keysInserted = 1;
+ CurOp::get(opCtx())->debug().additiveMetrics.keysInserted = 1;
+ txnParticipant->getSingleTransactionStats().getOpDebug()->additiveMetrics.keysDeleted = 0;
+ CurOp::get(opCtx())->debug().additiveMetrics.keysDeleted = 0;
+ txnParticipant->getSingleTransactionStats().getOpDebug()->additiveMetrics.prepareReadConflicts =
+ 5;
+ CurOp::get(opCtx())->debug().additiveMetrics.prepareReadConflicts = 4;
+
+ auto additiveMetricsToCompare =
+ txnParticipant->getSingleTransactionStats().getOpDebug()->additiveMetrics;
+ additiveMetricsToCompare.add(CurOp::get(opCtx())->debug().additiveMetrics);
+
+ txnParticipant->unstashTransactionResources(opCtx(), "insert");
+ // The transaction machinery cannot store an empty locker.
+ { Lock::GlobalLock lk(opCtx(), MODE_IX, Date_t::now(), Lock::InterruptBehavior::kThrow); }
+ txnParticipant->stashTransactionResources(opCtx());
+
+ ASSERT(txnParticipant->getSingleTransactionStats().getOpDebug()->additiveMetrics.equals(
+ additiveMetricsToCompare));
+}
+
+TEST_F(TransactionsMetricsTest, AdditiveMetricsObjectsShouldBeAddedTogetherUponCommit) {
+ OperationContextSessionMongod opCtxSession(opCtx(), true, false, true);
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+
+ // Initialize field values for both AdditiveMetrics objects.
+ txnParticipant->getSingleTransactionStats().getOpDebug()->additiveMetrics.keysExamined = 3;
+ CurOp::get(opCtx())->debug().additiveMetrics.keysExamined = 2;
+ txnParticipant->getSingleTransactionStats().getOpDebug()->additiveMetrics.docsExamined = 0;
+ CurOp::get(opCtx())->debug().additiveMetrics.docsExamined = 2;
+ txnParticipant->getSingleTransactionStats().getOpDebug()->additiveMetrics.nMatched = 4;
+ txnParticipant->getSingleTransactionStats().getOpDebug()->additiveMetrics.nModified = 5;
+ CurOp::get(opCtx())->debug().additiveMetrics.nModified = 1;
+ CurOp::get(opCtx())->debug().additiveMetrics.ninserted = 1;
+ txnParticipant->getSingleTransactionStats().getOpDebug()->additiveMetrics.ndeleted = 4;
+ CurOp::get(opCtx())->debug().additiveMetrics.ndeleted = 0;
+ txnParticipant->getSingleTransactionStats().getOpDebug()->additiveMetrics.keysInserted = 1;
+ CurOp::get(opCtx())->debug().additiveMetrics.keysInserted = 1;
+ txnParticipant->getSingleTransactionStats().getOpDebug()->additiveMetrics.prepareReadConflicts =
+ 0;
+ CurOp::get(opCtx())->debug().additiveMetrics.prepareReadConflicts = 0;
+ txnParticipant->getSingleTransactionStats().getOpDebug()->additiveMetrics.writeConflicts = 6;
+ CurOp::get(opCtx())->debug().additiveMetrics.writeConflicts = 3;
+
+ auto additiveMetricsToCompare =
+ txnParticipant->getSingleTransactionStats().getOpDebug()->additiveMetrics;
+ additiveMetricsToCompare.add(CurOp::get(opCtx())->debug().additiveMetrics);
+
+ txnParticipant->unstashTransactionResources(opCtx(), "insert");
+ // The transaction machinery cannot store an empty locker.
+ { Lock::GlobalLock lk(opCtx(), MODE_IX, Date_t::now(), Lock::InterruptBehavior::kThrow); }
+ txnParticipant->commitUnpreparedTransaction(opCtx());
+
+ ASSERT(txnParticipant->getSingleTransactionStats().getOpDebug()->additiveMetrics.equals(
+ additiveMetricsToCompare));
+}
+
+TEST_F(TransactionsMetricsTest, AdditiveMetricsObjectsShouldBeAddedTogetherUponAbort) {
+ OperationContextSessionMongod opCtxSession(opCtx(), true, false, true);
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+
+ // Initialize field values for both AdditiveMetrics objects.
+ txnParticipant->getSingleTransactionStats().getOpDebug()->additiveMetrics.keysExamined = 2;
+ CurOp::get(opCtx())->debug().additiveMetrics.keysExamined = 4;
+ txnParticipant->getSingleTransactionStats().getOpDebug()->additiveMetrics.docsExamined = 1;
+ CurOp::get(opCtx())->debug().additiveMetrics.docsExamined = 3;
+ txnParticipant->getSingleTransactionStats().getOpDebug()->additiveMetrics.nMatched = 2;
+ txnParticipant->getSingleTransactionStats().getOpDebug()->additiveMetrics.nModified = 0;
+ CurOp::get(opCtx())->debug().additiveMetrics.nModified = 3;
+ CurOp::get(opCtx())->debug().additiveMetrics.ndeleted = 5;
+ txnParticipant->getSingleTransactionStats().getOpDebug()->additiveMetrics.nmoved = 0;
+ CurOp::get(opCtx())->debug().additiveMetrics.nmoved = 2;
+ txnParticipant->getSingleTransactionStats().getOpDebug()->additiveMetrics.keysInserted = 1;
+ CurOp::get(opCtx())->debug().additiveMetrics.keysInserted = 1;
+ txnParticipant->getSingleTransactionStats().getOpDebug()->additiveMetrics.keysDeleted = 6;
+ CurOp::get(opCtx())->debug().additiveMetrics.keysDeleted = 0;
+ txnParticipant->getSingleTransactionStats().getOpDebug()->additiveMetrics.writeConflicts = 3;
+ CurOp::get(opCtx())->debug().additiveMetrics.writeConflicts = 3;
+
+ auto additiveMetricsToCompare =
+ txnParticipant->getSingleTransactionStats().getOpDebug()->additiveMetrics;
+ additiveMetricsToCompare.add(CurOp::get(opCtx())->debug().additiveMetrics);
+
+ txnParticipant->unstashTransactionResources(opCtx(), "insert");
+ // The transaction machinery cannot store an empty locker.
+ { Lock::GlobalLock lk(opCtx(), MODE_IX, Date_t::now(), Lock::InterruptBehavior::kThrow); }
+ txnParticipant->abortActiveTransaction(opCtx());
+
+ ASSERT(txnParticipant->getSingleTransactionStats().getOpDebug()->additiveMetrics.equals(
+ additiveMetricsToCompare));
+}
+
+TEST_F(TransactionsMetricsTest, TimeInactiveMicrosShouldBeSetUponUnstashAndStash) {
+ OperationContextSessionMongod opCtxSession(opCtx(), true, false, true);
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+
+ // Time inactive should be greater than or equal to zero.
+ ASSERT_GTE(txnParticipant->getSingleTransactionStats().getTimeInactiveMicros(curTimeMicros64()),
+ Microseconds{0});
+
+ // Save time inactive at this point.
+ auto timeInactiveSoFar =
+ txnParticipant->getSingleTransactionStats().getTimeInactiveMicros(curTimeMicros64());
+ // Sleep here to allow enough time to elapse.
+ sleepmillis(1);
+
+ // Time inactive should have increased.
+ ASSERT_GT(txnParticipant->getSingleTransactionStats().getTimeInactiveMicros(curTimeMicros64()),
+ timeInactiveSoFar);
+
+ timeInactiveSoFar =
+ txnParticipant->getSingleTransactionStats().getTimeInactiveMicros(curTimeMicros64());
+ // Sleep here to allow enough time to elapse.
+ sleepmillis(1);
+
+ // The transaction is still inactive, so time inactive should have increased.
+ ASSERT_GT(txnParticipant->getSingleTransactionStats().getTimeInactiveMicros(curTimeMicros64()),
+ timeInactiveSoFar);
+
+ txnParticipant->unstashTransactionResources(opCtx(), "insert");
+
+ timeInactiveSoFar =
+ txnParticipant->getSingleTransactionStats().getTimeInactiveMicros(curTimeMicros64());
+ // Sleep here to allow enough time to elapse.
+ sleepmillis(1);
+
+ // The transaction is currently active, so time inactive should not have increased.
+ ASSERT_EQ(txnParticipant->getSingleTransactionStats().getTimeInactiveMicros(curTimeMicros64()),
+ timeInactiveSoFar);
+
+ // The transaction machinery cannot store an empty locker.
+ { Lock::GlobalLock lk(opCtx(), MODE_IX, Date_t::now(), Lock::InterruptBehavior::kThrow); }
+ txnParticipant->stashTransactionResources(opCtx());
+
+ // The transaction is inactive again, so time inactive should have increased.
+ ASSERT_GT(txnParticipant->getSingleTransactionStats().getTimeInactiveMicros(curTimeMicros64()),
+ timeInactiveSoFar);
+}
+
+TEST_F(TransactionsMetricsTest, TimeInactiveMicrosShouldBeSetUponUnstashAndAbort) {
+ OperationContextSessionMongod opCtxSession(opCtx(), true, false, true);
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+
+ // Time inactive should be greater than or equal to zero.
+ ASSERT_GTE(txnParticipant->getSingleTransactionStats().getTimeInactiveMicros(curTimeMicros64()),
+ Microseconds{0});
+
+ // Save time inactive at this point.
+ auto timeInactiveSoFar =
+ txnParticipant->getSingleTransactionStats().getTimeInactiveMicros(curTimeMicros64());
+ // Sleep here to allow enough time to elapse.
+ sleepmillis(1);
+
+ // Time inactive should have increased.
+ ASSERT_GT(txnParticipant->getSingleTransactionStats().getTimeInactiveMicros(curTimeMicros64()),
+ timeInactiveSoFar);
+
+ txnParticipant->unstashTransactionResources(opCtx(), "insert");
+ txnParticipant->abortArbitraryTransaction();
+
+ timeInactiveSoFar =
+ txnParticipant->getSingleTransactionStats().getTimeInactiveMicros(curTimeMicros64());
+ // Sleep here to allow enough time to elapse.
+ sleepmillis(1);
+
+ // The transaction has aborted, so time inactive should not have increased.
+ ASSERT_EQ(txnParticipant->getSingleTransactionStats().getTimeInactiveMicros(curTimeMicros64()),
+ timeInactiveSoFar);
+}
+
+TEST_F(TransactionsMetricsTest, TimeInactiveMicrosShouldIncreaseUntilCommit) {
+ OperationContextSessionMongod opCtxSession(opCtx(), true, false, true);
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+
+ // Time inactive should be greater than or equal to zero.
+ ASSERT_GTE(txnParticipant->getSingleTransactionStats().getTimeInactiveMicros(curTimeMicros64()),
+ Microseconds{0});
+
+ // Save time inactive at this point.
+ auto timeInactiveSoFar =
+ txnParticipant->getSingleTransactionStats().getTimeInactiveMicros(curTimeMicros64());
+ // Sleep here to allow enough time to elapse.
+ sleepmillis(1);
+
+ // Time inactive should have increased.
+ ASSERT_GT(txnParticipant->getSingleTransactionStats().getTimeInactiveMicros(curTimeMicros64()),
+ timeInactiveSoFar);
+
+ txnParticipant->unstashTransactionResources(opCtx(), "insert");
+ // The transaction machinery cannot store an empty locker.
+ { Lock::GlobalLock lk(opCtx(), MODE_IX, Date_t::now(), Lock::InterruptBehavior::kThrow); }
+ txnParticipant->commitUnpreparedTransaction(opCtx());
+
+ timeInactiveSoFar =
+ txnParticipant->getSingleTransactionStats().getTimeInactiveMicros(curTimeMicros64());
+ // Sleep here to allow enough time to elapse.
+ sleepmillis(1);
+
+ // The transaction has committed, so time inactive should not have increased.
+ ASSERT_EQ(txnParticipant->getSingleTransactionStats().getTimeInactiveMicros(curTimeMicros64()),
+ timeInactiveSoFar);
+}
+
+namespace {
+
+/*
+ * Constructs a ClientMetadata BSONObj with the given application name.
+ */
+BSONObj constructClientMetadata(StringData appName) {
+ BSONObjBuilder builder;
+ ASSERT_OK(ClientMetadata::serializePrivate("driverName",
+ "driverVersion",
+ "osType",
+ "osName",
+ "osArchitecture",
+ "osVersion",
+ appName,
+ &builder));
+ return builder.obj();
+}
+} // namespace
+
+TEST_F(TransactionsMetricsTest, LastClientInfoShouldUpdateUponStash) {
+ // Create a ClientMetadata object and set it on ClientMetadataIsMasterState.
+ auto obj = constructClientMetadata("appName");
+ auto clientMetadata = ClientMetadata::parse(obj["client"]);
+ auto& clientMetadataIsMasterState = ClientMetadataIsMasterState::get(opCtx()->getClient());
+ clientMetadataIsMasterState.setClientMetadata(opCtx()->getClient(),
+ std::move(clientMetadata.getValue()));
+
+ OperationContextSessionMongod opCtxSession(opCtx(), true, false, true);
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+ txnParticipant->unstashTransactionResources(opCtx(), "insert");
+ // The transaction machinery cannot store an empty locker.
+ { Lock::GlobalLock lk(opCtx(), MODE_IX, Date_t::now(), Lock::InterruptBehavior::kThrow); }
+ txnParticipant->stashTransactionResources(opCtx());
+
+ // LastClientInfo should have been set.
+ ASSERT_EQ(txnParticipant->getSingleTransactionStats().getLastClientInfo().clientHostAndPort,
+ "");
+ ASSERT_EQ(txnParticipant->getSingleTransactionStats().getLastClientInfo().connectionId, 0);
+ ASSERT_EQ(txnParticipant->getSingleTransactionStats().getLastClientInfo().appName, "appName");
+ ASSERT_BSONOBJ_EQ(
+ txnParticipant->getSingleTransactionStats().getLastClientInfo().clientMetadata,
+ obj.getField("client").Obj());
+
+ // Create another ClientMetadata object.
+ auto newObj = constructClientMetadata("newAppName");
+ auto newClientMetadata = ClientMetadata::parse(newObj["client"]);
+ clientMetadataIsMasterState.setClientMetadata(opCtx()->getClient(),
+ std::move(newClientMetadata.getValue()));
+
+ txnParticipant->unstashTransactionResources(opCtx(), "insert");
+ txnParticipant->stashTransactionResources(opCtx());
+
+ // LastClientInfo's clientMetadata should have been updated to the new ClientMetadata object.
+ ASSERT_EQ(txnParticipant->getSingleTransactionStats().getLastClientInfo().appName,
+ "newAppName");
+ ASSERT_BSONOBJ_EQ(
+ txnParticipant->getSingleTransactionStats().getLastClientInfo().clientMetadata,
+ newObj.getField("client").Obj());
+}
+
+TEST_F(TransactionsMetricsTest, LastClientInfoShouldUpdateUponCommit) {
+ // Create a ClientMetadata object and set it on ClientMetadataIsMasterState.
+ auto obj = constructClientMetadata("appName");
+ auto clientMetadata = ClientMetadata::parse(obj["client"]);
+ auto& clientMetadataIsMasterState = ClientMetadataIsMasterState::get(opCtx()->getClient());
+ clientMetadataIsMasterState.setClientMetadata(opCtx()->getClient(),
+ std::move(clientMetadata.getValue()));
+
+ OperationContextSessionMongod opCtxSession(opCtx(), true, false, true);
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+ txnParticipant->unstashTransactionResources(opCtx(), "insert");
+ // The transaction machinery cannot store an empty locker.
+ Lock::GlobalLock lk(opCtx(), MODE_IX, Date_t::now(), Lock::InterruptBehavior::kThrow);
+ txnParticipant->commitUnpreparedTransaction(opCtx());
+
+ // LastClientInfo should have been set.
+ ASSERT_EQ(txnParticipant->getSingleTransactionStats().getLastClientInfo().clientHostAndPort,
+ "");
+ ASSERT_EQ(txnParticipant->getSingleTransactionStats().getLastClientInfo().connectionId, 0);
+ ASSERT_EQ(txnParticipant->getSingleTransactionStats().getLastClientInfo().appName, "appName");
+ ASSERT_BSONOBJ_EQ(
+ txnParticipant->getSingleTransactionStats().getLastClientInfo().clientMetadata,
+ obj.getField("client").Obj());
+}
+
+TEST_F(TransactionsMetricsTest, LastClientInfoShouldUpdateUponAbort) {
+ // Create a ClientMetadata object and set it on ClientMetadataIsMasterState.
+ auto obj = constructClientMetadata("appName");
+ auto clientMetadata = ClientMetadata::parse(obj["client"]);
+
+ auto& clientMetadataIsMasterState = ClientMetadataIsMasterState::get(opCtx()->getClient());
+ clientMetadataIsMasterState.setClientMetadata(opCtx()->getClient(),
+ std::move(clientMetadata.getValue()));
+
+ OperationContextSessionMongod opCtxSession(opCtx(), true, false, true);
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+ txnParticipant->unstashTransactionResources(opCtx(), "insert");
+ txnParticipant->abortActiveTransaction(opCtx());
+
+ // LastClientInfo should have been set.
+ ASSERT_EQ(txnParticipant->getSingleTransactionStats().getLastClientInfo().clientHostAndPort,
+ "");
+ ASSERT_EQ(txnParticipant->getSingleTransactionStats().getLastClientInfo().connectionId, 0);
+ ASSERT_EQ(txnParticipant->getSingleTransactionStats().getLastClientInfo().appName, "appName");
+ ASSERT_BSONOBJ_EQ(
+ txnParticipant->getSingleTransactionStats().getLastClientInfo().clientMetadata,
+ obj.getField("client").Obj());
+}
+
+/*
+ * Sets up the additive metrics for Transactions Metrics test.
+ */
+void setupAdditiveMetrics(const int metricValue, OperationContext* opCtx) {
+ CurOp::get(opCtx)->debug().additiveMetrics.keysExamined = metricValue;
+ CurOp::get(opCtx)->debug().additiveMetrics.docsExamined = metricValue;
+ CurOp::get(opCtx)->debug().additiveMetrics.nMatched = metricValue;
+ CurOp::get(opCtx)->debug().additiveMetrics.nModified = metricValue;
+ CurOp::get(opCtx)->debug().additiveMetrics.ninserted = metricValue;
+ CurOp::get(opCtx)->debug().additiveMetrics.ndeleted = metricValue;
+ CurOp::get(opCtx)->debug().additiveMetrics.nmoved = metricValue;
+ CurOp::get(opCtx)->debug().additiveMetrics.keysInserted = metricValue;
+ CurOp::get(opCtx)->debug().additiveMetrics.keysDeleted = metricValue;
+ CurOp::get(opCtx)->debug().additiveMetrics.prepareReadConflicts = metricValue;
+ CurOp::get(opCtx)->debug().additiveMetrics.writeConflicts = metricValue;
+}
+
+/*
+ * Builds expected parameters info string.
+ */
+void buildParametersInfoString(StringBuilder* sb,
+ LogicalSessionId sessionId,
+ const TxnNumber txnNum,
+ const repl::ReadConcernArgs readConcernArgs) {
+ BSONObjBuilder lsidBuilder;
+ sessionId.serialize(&lsidBuilder);
+ (*sb) << "parameters:{ lsid: " << lsidBuilder.done().toString() << ", txnNumber: " << txnNum
+ << ", autocommit: false"
+ << ", readConcern: " << readConcernArgs.toBSON().getObjectField("readConcern") << " },";
+}
+
+/*
+ * Builds expected single transaction stats info string.
+ */
+void buildSingleTransactionStatsString(StringBuilder* sb, const int metricValue) {
+ (*sb) << " keysExamined:" << metricValue << " docsExamined:" << metricValue
+ << " nMatched:" << metricValue << " nModified:" << metricValue
+ << " ninserted:" << metricValue << " ndeleted:" << metricValue
+ << " nmoved:" << metricValue << " keysInserted:" << metricValue
+ << " keysDeleted:" << metricValue << " prepareReadConflicts:" << metricValue
+ << " writeConflicts:" << metricValue;
+}
+
+/*
+ * Builds the time active and time inactive info string.
+ */
+void buildTimeActiveInactiveString(StringBuilder* sb,
+ TransactionParticipant* txnParticipant,
+ unsigned long long curTime) {
+ // Add time active micros to string.
+ (*sb) << " timeActiveMicros:"
+ << durationCount<Microseconds>(
+ txnParticipant->getSingleTransactionStats().getTimeActiveMicros(curTime));
+
+ // Add time inactive micros to string.
+ (*sb) << " timeInactiveMicros:"
+ << durationCount<Microseconds>(
+ txnParticipant->getSingleTransactionStats().getTimeInactiveMicros(curTime));
+}
+
+/*
+ * Builds the entire expected transaction info string and returns it.
+ */
+std::string buildTransactionInfoString(OperationContext* opCtx,
+ TransactionParticipant* txnParticipant,
+ std::string terminationCause,
+ const LogicalSessionId sessionId,
+ const TxnNumber txnNum,
+ const int metricValue) {
+ // Calling transactionInfoForLog to get the actual transaction info string.
+ const auto lockerInfo = opCtx->lockState()->getLockerInfo();
+ // Building expected transaction info string.
+ StringBuilder parametersInfo;
+ buildParametersInfoString(
+ &parametersInfo, sessionId, txnNum, repl::ReadConcernArgs::get(opCtx));
+
+ StringBuilder readTimestampInfo;
+ readTimestampInfo
+ << " readTimestamp:"
+ << txnParticipant->getSpeculativeTransactionReadOpTimeForTest().getTimestamp().toString()
+ << ",";
+
+ StringBuilder singleTransactionStatsInfo;
+ buildSingleTransactionStatsString(&singleTransactionStatsInfo, metricValue);
+
+ auto curTime = curTimeMicros64();
+ StringBuilder timeActiveAndInactiveInfo;
+ buildTimeActiveInactiveString(&timeActiveAndInactiveInfo, txnParticipant, curTime);
+
+ BSONObjBuilder locks;
+ if (lockerInfo) {
+ lockerInfo->stats.report(&locks);
+ }
+
+ // Puts all the substrings together into one expected info string. The expected info string will
+ // look something like this:
+ // parameters:{ lsid: { id: UUID("f825288c-100e-49a1-9fd7-b95c108049e6"), uid: BinData(0,
+ // E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855) }, txnNumber: 1,
+ // autocommit: false }, readTimestamp:Timestamp(0, 0), keysExamined:1 docsExamined:1 nMatched:1
+ // nModified:1 ninserted:1 ndeleted:1 nmoved:1 keysInserted:1 keysDeleted:1
+ // prepareReadConflicts:1 writeConflicts:1 terminationCause:committed timeActiveMicros:3
+ // timeInactiveMicros:2 numYields:0 locks:{ Global: { acquireCount: { r: 6, w: 4 } }, Database:
+ // { acquireCount: { r: 1, w: 1, W: 2 } }, Collection: { acquireCount: { R: 1 } }, oplog: {
+ // acquireCount: { W: 1 } } } 0ms
+ StringBuilder expectedTransactionInfo;
+ expectedTransactionInfo
+ << parametersInfo.str() << readTimestampInfo.str() << singleTransactionStatsInfo.str()
+ << " terminationCause:" << terminationCause << timeActiveAndInactiveInfo.str()
+ << " numYields:" << 0 << " locks:" << locks.done().toString() << " "
+ << Milliseconds{static_cast<long long>(
+ txnParticipant->getSingleTransactionStats().getDuration(curTime)) /
+ 1000};
+ return expectedTransactionInfo.str();
+}
+
+TEST_F(TransactionsMetricsTest, TestTransactionInfoForLogAfterCommit) {
+ // Initialize SingleTransactionStats AdditiveMetrics objects.
+ const int metricValue = 1;
+ setupAdditiveMetrics(metricValue, opCtx());
+
+ OperationContextSessionMongod opCtxSession(opCtx(), true, false, true);
+
+ repl::ReadConcernArgs readConcernArgs;
+ ASSERT_OK(readConcernArgs.initialize(BSON("find"
+ << "test"
+ << repl::ReadConcernArgs::kReadConcernFieldName
+ << BSON(repl::ReadConcernArgs::kLevelFieldName
+ << "snapshot"))));
+
+ repl::ReadConcernArgs::get(opCtx()) = readConcernArgs;
+
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+
+ txnParticipant->unstashTransactionResources(opCtx(), "commitTransaction");
+ txnParticipant->commitUnpreparedTransaction(opCtx());
+
+ const auto lockerInfo = opCtx()->lockState()->getLockerInfo();
+ ASSERT(lockerInfo);
+ std::string testTransactionInfo =
+ txnParticipant->transactionInfoForLogForTest(&lockerInfo->stats, true, readConcernArgs);
+
+ std::string expectedTransactionInfo =
+ buildTransactionInfoString(opCtx(),
+ txnParticipant,
+ "committed",
+ *opCtx()->getLogicalSessionId(),
+ *opCtx()->getTxnNumber(),
+ metricValue);
+
+ ASSERT_EQ(testTransactionInfo, expectedTransactionInfo);
+}
+
+TEST_F(TransactionsMetricsTest, TestTransactionInfoForLogAfterAbort) {
+ // Initialize SingleTransactionStats AdditiveMetrics objects.
+ const int metricValue = 1;
+ setupAdditiveMetrics(metricValue, opCtx());
+
+ OperationContextSessionMongod opCtxSession(opCtx(), true, false, true);
+
+ repl::ReadConcernArgs readConcernArgs;
+ ASSERT_OK(readConcernArgs.initialize(BSON("find"
+ << "test"
+ << repl::ReadConcernArgs::kReadConcernFieldName
+ << BSON(repl::ReadConcernArgs::kLevelFieldName
+ << "snapshot"))));
+ repl::ReadConcernArgs::get(opCtx()) = readConcernArgs;
+
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+
+ txnParticipant->unstashTransactionResources(opCtx(), "abortTransaction");
+ txnParticipant->abortActiveTransaction(opCtx());
+
+ const auto lockerInfo = opCtx()->lockState()->getLockerInfo();
+ ASSERT(lockerInfo);
+
+ std::string testTransactionInfo =
+ txnParticipant->transactionInfoForLogForTest(&lockerInfo->stats, false, readConcernArgs);
+
+ std::string expectedTransactionInfo =
+ buildTransactionInfoString(opCtx(),
+ txnParticipant,
+ "aborted",
+ *opCtx()->getLogicalSessionId(),
+ *opCtx()->getTxnNumber(),
+ metricValue);
+
+ ASSERT_EQ(testTransactionInfo, expectedTransactionInfo);
+}
+
+DEATH_TEST_F(TransactionsMetricsTest, TestTransactionInfoForLogWithNoLockerInfoStats, "invariant") {
+ OperationContextSessionMongod opCtxSession(opCtx(), true, false, true);
+
+ repl::ReadConcernArgs readConcernArgs;
+ ASSERT_OK(readConcernArgs.initialize(BSON("find"
+ << "test"
+ << repl::ReadConcernArgs::kReadConcernFieldName
+ << BSON(repl::ReadConcernArgs::kLevelFieldName
+ << "snapshot"))));
+ repl::ReadConcernArgs::get(opCtx()) = readConcernArgs;
+
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+
+ const auto lockerInfo = opCtx()->lockState()->getLockerInfo();
+ ASSERT(lockerInfo);
+
+ txnParticipant->unstashTransactionResources(opCtx(), "commitTransaction");
+ txnParticipant->commitUnpreparedTransaction(opCtx());
+
+ txnParticipant->transactionInfoForLogForTest(nullptr, true, readConcernArgs);
+}
+
+TEST_F(TransactionsMetricsTest, LogTransactionInfoAfterSlowCommit) {
+ OperationContextSessionMongod opCtxSession(opCtx(), true, false, true);
+
+ repl::ReadConcernArgs readConcernArgs;
+ ASSERT_OK(readConcernArgs.initialize(BSON("find"
+ << "test"
+ << repl::ReadConcernArgs::kReadConcernFieldName
+ << BSON(repl::ReadConcernArgs::kLevelFieldName
+ << "snapshot"))));
+ repl::ReadConcernArgs::get(opCtx()) = readConcernArgs;
+
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+
+ // Initialize SingleTransactionStats AdditiveMetrics objects.
+ const int metricValue = 1;
+ setupAdditiveMetrics(metricValue, opCtx());
+
+ txnParticipant->unstashTransactionResources(opCtx(), "commitTransaction");
+
+ serverGlobalParams.slowMS = 10;
+ sleepmillis(serverGlobalParams.slowMS + 1);
+
+ startCapturingLogMessages();
+ txnParticipant->commitUnpreparedTransaction(opCtx());
+ stopCapturingLogMessages();
+
+ const auto lockerInfo = opCtx()->lockState()->getLockerInfo();
+ ASSERT(lockerInfo);
+ std::string expectedTransactionInfo = "transaction " +
+ txnParticipant->transactionInfoForLogForTest(&lockerInfo->stats, true, readConcernArgs);
+ ASSERT_EQUALS(1, countLogLinesContaining(expectedTransactionInfo));
+}
+
+TEST_F(TransactionsMetricsTest, LogTransactionInfoAfterSlowAbort) {
+ OperationContextSessionMongod opCtxSession(opCtx(), true, false, true);
+
+ repl::ReadConcernArgs readConcernArgs;
+ ASSERT_OK(readConcernArgs.initialize(BSON("find"
+ << "test"
+ << repl::ReadConcernArgs::kReadConcernFieldName
+ << BSON(repl::ReadConcernArgs::kLevelFieldName
+ << "snapshot"))));
+ repl::ReadConcernArgs::get(opCtx()) = readConcernArgs;
+
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+
+ // Initialize SingleTransactionStats AdditiveMetrics objects.
+ const int metricValue = 1;
+ setupAdditiveMetrics(metricValue, opCtx());
+
+ txnParticipant->unstashTransactionResources(opCtx(), "abortTransaction");
+
+ serverGlobalParams.slowMS = 10;
+ sleepmillis(serverGlobalParams.slowMS + 1);
+
+ startCapturingLogMessages();
+ txnParticipant->abortActiveTransaction(opCtx());
+ stopCapturingLogMessages();
+
+ const auto lockerInfo = opCtx()->lockState()->getLockerInfo();
+ ASSERT(lockerInfo);
+ std::string expectedTransactionInfo = "transaction " +
+ txnParticipant->transactionInfoForLogForTest(&lockerInfo->stats, false, readConcernArgs);
+ ASSERT_EQUALS(1, countLogLinesContaining(expectedTransactionInfo));
+}
+
+TEST_F(TransactionsMetricsTest, LogTransactionInfoAfterSlowStashedAbort) {
+ OperationContextSessionMongod opCtxSession(opCtx(), true, false, true);
+
+ repl::ReadConcernArgs readConcernArgs;
+ ASSERT_OK(readConcernArgs.initialize(BSON("find"
+ << "test"
+ << repl::ReadConcernArgs::kReadConcernFieldName
+ << BSON(repl::ReadConcernArgs::kLevelFieldName
+ << "snapshot"))));
+ repl::ReadConcernArgs::get(opCtx()) = readConcernArgs;
+
+ auto txnParticipant = TransactionParticipant::get(opCtx());
+
+ // Initialize SingleTransactionStats AdditiveMetrics objects.
+ const int metricValue = 1;
+ setupAdditiveMetrics(metricValue, opCtx());
+
+ txnParticipant->unstashTransactionResources(opCtx(), "insert");
+
+ { Lock::GlobalLock lk(opCtx(), MODE_IX, Date_t::now(), Lock::InterruptBehavior::kThrow); }
+
+ txnParticipant->stashTransactionResources(opCtx());
+ const auto txnResourceStashLocker = txnParticipant->getTxnResourceStashLockerForTest();
+ ASSERT(txnResourceStashLocker);
+ const auto lockerInfo = txnResourceStashLocker->getLockerInfo();
+
+ serverGlobalParams.slowMS = 10;
+ sleepmillis(serverGlobalParams.slowMS + 1);
+
+ startCapturingLogMessages();
+ txnParticipant->abortArbitraryTransaction();
+ stopCapturingLogMessages();
+
+ std::string expectedTransactionInfo = "transaction " +
+ txnParticipant->transactionInfoForLogForTest(&lockerInfo->stats, false, readConcernArgs);
+ ASSERT_EQUALS(1, countLogLinesContaining(expectedTransactionInfo));
+}
+
+} // namespace
+} // namespace mongo