diff options
author | Randolph Tan <randolph@10gen.com> | 2018-06-29 10:19:48 -0400 |
---|---|---|
committer | Randolph Tan <randolph@10gen.com> | 2018-08-08 16:00:20 -0400 |
commit | 52b2cc0886cdb992e2491067bdc029301d5bb6af (patch) | |
tree | c414da256a84890af3fb084ab699b572b49dab4f /src/mongo/db | |
parent | 210bb5d91cb3c77bb3ed169114f8b85cd1062fb3 (diff) | |
download | mongo-52b2cc0886cdb992e2491067bdc029301d5bb6af.tar.gz |
SERVER-35900 Refactor transaction machinery out from Session class
Diffstat (limited to 'src/mongo/db')
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(¶metersBuilder); - 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(¶metersBuilder); - 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( - ¶metersInfo, 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(¶metersBuilder); + 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(¶metersBuilder); + + 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( + ¶metersInfo, 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 |