diff options
author | Bernard Gorman <bernard.gorman@gmail.com> | 2018-03-28 19:00:18 +0100 |
---|---|---|
committer | Bernard Gorman <bernard.gorman@gmail.com> | 2018-04-05 03:05:56 +0100 |
commit | 75160508e02cb5051f554baff3e67f2adb316057 (patch) | |
tree | fdf97c30fdcd0ec172470f43f4cc4aa6b66dcdb1 | |
parent | f00b908d2bf6cca4c2527eaa88b0ae79d745fd0b (diff) | |
download | mongo-75160508e02cb5051f554baff3e67f2adb316057.tar.gz |
SERVER-33294 Report stashed in-use locks for idle sessions in currentOp
-rw-r--r-- | jstests/sharding/aggregation_currentop.js | 101 | ||||
-rw-r--r-- | src/mongo/db/SConscript | 2 | ||||
-rw-r--r-- | src/mongo/db/curop.cpp | 8 | ||||
-rw-r--r-- | src/mongo/db/kill_sessions.cpp | 14 | ||||
-rw-r--r-- | src/mongo/db/kill_sessions.h | 6 | ||||
-rw-r--r-- | src/mongo/db/pipeline/document_source_current_op.cpp | 37 | ||||
-rw-r--r-- | src/mongo/db/pipeline/document_source_current_op.h | 13 | ||||
-rw-r--r-- | src/mongo/db/pipeline/document_source_current_op_test.cpp | 1 | ||||
-rw-r--r-- | src/mongo/db/pipeline/mongo_process_common.cpp | 6 | ||||
-rw-r--r-- | src/mongo/db/pipeline/mongo_process_common.h | 9 | ||||
-rw-r--r-- | src/mongo/db/pipeline/mongo_process_interface.h | 2 | ||||
-rw-r--r-- | src/mongo/db/pipeline/pipeline_d.cpp | 31 | ||||
-rw-r--r-- | src/mongo/db/pipeline/pipeline_d.h | 4 | ||||
-rw-r--r-- | src/mongo/db/pipeline/stub_mongo_process_interface.h | 1 | ||||
-rw-r--r-- | src/mongo/db/session.cpp | 28 | ||||
-rw-r--r-- | src/mongo/db/session.h | 19 | ||||
-rw-r--r-- | src/mongo/db/session_test.cpp | 73 | ||||
-rw-r--r-- | src/mongo/s/commands/pipeline_s.h | 7 |
18 files changed, 344 insertions, 18 deletions
diff --git a/jstests/sharding/aggregation_currentop.js b/jstests/sharding/aggregation_currentop.js index 5254619d2d7..ec7e92cb779 100644 --- a/jstests/sharding/aggregation_currentop.js +++ b/jstests/sharding/aggregation_currentop.js @@ -54,6 +54,7 @@ let clusterTestDB = mongosConn.getDB(jsTestName()); let clusterAdminDB = mongosConn.getDB("admin"); shardConn.waitForClusterTime(60); + let shardTestDB = shardConn.getDB(jsTestName()); let shardAdminDB = shardConn.getDB("admin"); function createUsers(conn) { @@ -70,11 +71,14 @@ privileges: [{resource: {cluster: true}, actions: ["inprog"]}] })); - assert.commandWorked( - adminDB.runCommand({createUser: "user_inprog", pwd: "pwd", roles: ["role_inprog"]})); + assert.commandWorked(adminDB.runCommand({ + createUser: "user_inprog", + pwd: "pwd", + roles: ["readWriteAnyDatabase", "role_inprog"] + })); assert.commandWorked(adminDB.runCommand( - {createUser: "user_no_inprog", pwd: "pwd", roles: ["readAnyDatabase"]})); + {createUser: "user_no_inprog", pwd: "pwd", roles: ["readWriteAnyDatabase"]})); } // Create necessary users at both cluster and shard-local level. @@ -115,6 +119,7 @@ shardRS = st.rs0; clusterTestDB = mongosConn.getDB(jsTestName()); clusterAdminDB = mongosConn.getDB("admin"); + shardTestDB = shardConn.getDB(jsTestName()); shardAdminDB = shardConn.getDB("admin"); } @@ -602,6 +607,95 @@ runLocalOpsTests(shardConn); // + // Stashed transactions tests. + // + + // Test that $currentOp will display stashed transaction locks if 'idleSessions' is true, and + // will only permit a user to view other users' sessions if the caller possesses the 'inprog' + // privilege and 'allUsers' is true. + const userNames = ["user_inprog", "admin", "user_no_inprog"]; + const sessionDBs = []; + const sessions = []; + + // Returns a set of predicates that filter $currentOp for all stashed transactions. + function sessionFilter() { + return { + active: false, + opid: {$exists: false}, + desc: "inactive transaction", + "lsid.id": {$in: sessions.map((session) => session.getSessionId().id)}, + txnNumber: {$gte: 0, $lt: sessions.length} + }; + } + + for (let i in userNames) { + shardAdminDB.logout(); + assert(shardAdminDB.auth(userNames[i], "pwd")); + + // Create a session for this user. + const session = shardAdminDB.getMongo().startSession(); + + // For each session, start but do not complete a transaction. + const sessionDB = session.getDatabase(shardTestDB.getName()); + assert.commandWorked(sessionDB.runCommand({ + insert: "test", + documents: [{_id: `txn-insert-${userNames[i]}-${i}`}], + readConcern: {level: "snapshot"}, + txnNumber: NumberLong(i), + startTransaction: true, + autocommit: false + })); + sessionDBs.push(sessionDB); + sessions.push(session); + + // Use $currentOp to confirm that the incomplete transactions have stashed their locks while + // inactive, and that each user can only view their own sessions with 'allUsers:false'. + assert.eq(shardAdminDB + .aggregate([ + {$currentOp: {allUsers: false, idleSessions: true}}, + {$match: sessionFilter()} + ]) + .itcount(), + 1); + } + + // Log in as 'user_no_inprog' to verify that the user cannot view other users' sessions via + // 'allUsers:true'. + shardAdminDB.logout(); + assert(shardAdminDB.auth("user_no_inprog", "pwd")); + + assert.commandFailedWithCode(shardAdminDB.runCommand({ + aggregate: 1, + cursor: {}, + pipeline: [{$currentOp: {allUsers: true, idleSessions: true}}, {$match: sessionFilter()}] + }), + ErrorCodes.Unauthorized); + + // Log in as 'user_inprog' to confirm that a user with the 'inprog' privilege can see all three + // stashed transactions with 'allUsers:true'. + shardAdminDB.logout(); + assert(shardAdminDB.auth("user_inprog", "pwd")); + + assert.eq( + shardAdminDB + .aggregate( + [{$currentOp: {allUsers: true, idleSessions: true}}, {$match: sessionFilter()}]) + .itcount(), + 3); + + // Allow all transactions to complete and close the associated sessions. + for (let i in userNames) { + assert(shardAdminDB.auth(userNames[i], "pwd")); + assert.commandWorked(sessionDBs[i].adminCommand({ + commitTransaction: 1, + txnNumber: NumberLong(i), + autocommit: false, + writeConcern: {w: 'majority'} + })); + sessions[i].endSession(); + } + + // // No-auth tests. // @@ -655,6 +749,7 @@ // Take the replica set out of the cluster. shardConn = restartReplSet(st.rs0, {shardsvr: null}); + shardTestDB = shardConn.getDB(jsTestName()); shardAdminDB = shardConn.getDB("admin"); // Test that the host field is present and the shard field is absent when run on mongoD. diff --git a/src/mongo/db/SConscript b/src/mongo/db/SConscript index 7c0d43768ef..17add6e4bf7 100644 --- a/src/mongo/db/SConscript +++ b/src/mongo/db/SConscript @@ -606,6 +606,7 @@ env.Library( env.Idlc('transactions_stats.idl')[0], ], LIBDEPS=[ + '$BUILD_DIR/mongo/db/stats/fill_locker_info', '$BUILD_DIR/mongo/idl/idl_parser', 'catalog/collection', 'catalog/database', @@ -636,6 +637,7 @@ env.CppUnitTest( 'catalog/database_holder_mock', 'catalog_raii', 'service_context_noop_init', + 'stats/fill_locker_info', ], ) diff --git a/src/mongo/db/curop.cpp b/src/mongo/db/curop.cpp index 1381b76cf29..9e5fd5582af 100644 --- a/src/mongo/db/curop.cpp +++ b/src/mongo/db/curop.cpp @@ -260,9 +260,13 @@ void CurOp::reportCurrentOpForClient(OperationContext* opCtx, infoBuilder->append("killPending", true); } - if (clientOpCtx->getLogicalSessionId()) { + if (auto lsid = clientOpCtx->getLogicalSessionId()) { BSONObjBuilder bob(infoBuilder->subobjStart("lsid")); - clientOpCtx->getLogicalSessionId()->serialize(&bob); + lsid->serialize(&bob); + } + + if (auto txnNumber = clientOpCtx->getTxnNumber()) { + infoBuilder->append("txnNumber", *txnNumber); } CurOp::get(clientOpCtx)->reportState(infoBuilder, truncateOps); diff --git a/src/mongo/db/kill_sessions.cpp b/src/mongo/db/kill_sessions.cpp index f16202c343e..1ac38112c3d 100644 --- a/src/mongo/db/kill_sessions.cpp +++ b/src/mongo/db/kill_sessions.cpp @@ -120,6 +120,20 @@ KillAllSessionsByPattern makeKillAllSessionsByPattern(OperationContext* opCtx, return kasbp; } +KillAllSessionsByPatternSet makeSessionFilterForAuthenticatedUsers(OperationContext* opCtx) { + AuthorizationSession* authSession = AuthorizationSession::get(opCtx->getClient()); + KillAllSessionsByPatternSet patterns; + + for (auto it = authSession->getAuthenticatedUserNames(); it.more(); it.next()) { + if (auto user = authSession->lookupUser(*it)) { + KillAllSessionsByPattern pattern; + pattern.setUid(user->getDigest()); + patterns.emplace(std::move(pattern)); + } + } + return patterns; +} + KillAllSessionsByPattern makeKillAllSessionsByPattern(OperationContext* opCtx, const LogicalSessionId& lsid) { KillAllSessionsByPattern kasbp = makeKillAllSessionsByPattern(opCtx); diff --git a/src/mongo/db/kill_sessions.h b/src/mongo/db/kill_sessions.h index 547c2d038a3..40f6fabcc4b 100644 --- a/src/mongo/db/kill_sessions.h +++ b/src/mongo/db/kill_sessions.h @@ -94,6 +94,12 @@ KillAllSessionsByPattern makeKillAllSessionsByPattern(OperationContext* opCtx, const KillAllSessionsUser& user); /** + * Constructs a KillAllSessionsByPatternSet, each element of which matches the UID of a user that is + * currently authenticated on the given connection. + */ +KillAllSessionsByPatternSet makeSessionFilterForAuthenticatedUsers(OperationContext* opCtx); + +/** * Constructs a kill sessions pattern for a particular logical session */ KillAllSessionsByPattern makeKillAllSessionsByPattern(OperationContext* opCtx, diff --git a/src/mongo/db/pipeline/document_source_current_op.cpp b/src/mongo/db/pipeline/document_source_current_op.cpp index 5d9eb824adf..3c982d53487 100644 --- a/src/mongo/db/pipeline/document_source_current_op.cpp +++ b/src/mongo/db/pipeline/document_source_current_op.cpp @@ -37,6 +37,7 @@ namespace mongo { namespace { const StringData kAllUsersFieldName = "allUsers"_sd; const StringData kIdleConnectionsFieldName = "idleConnections"_sd; +const StringData kIdleSessionsFieldName = "idleSessions"_sd; const StringData kLocalOpsFieldName = "localOps"_sd; const StringData kTruncateOpsFieldName = "truncateOps"_sd; @@ -108,8 +109,11 @@ DocumentSource::GetNextResult DocumentSourceCurrentOp::getNext() { pExpCtx->checkForInterrupt(); if (_ops.empty()) { - _ops = pExpCtx->mongoProcessInterface->getCurrentOps( - pExpCtx->opCtx, _includeIdleConnections, _includeOpsFromAllUsers, _truncateOps); + _ops = pExpCtx->mongoProcessInterface->getCurrentOps(pExpCtx->opCtx, + _includeIdleConnections, + _includeIdleSessions, + _includeOpsFromAllUsers, + _truncateOps); _opsIter = _ops.begin(); @@ -180,6 +184,7 @@ intrusive_ptr<DocumentSource> DocumentSourceCurrentOp::createFromBson( nss.db() == NamespaceString::kAdminDb && nss.isCollectionlessAggregateNS()); ConnMode includeIdleConnections = ConnMode::kExcludeIdle; + SessionMode includeIdleSessions = SessionMode::kIncludeIdle; UserMode includeOpsFromAllUsers = UserMode::kExcludeOthers; LocalOpsMode showLocalOpsOnMongoS = LocalOpsMode::kRemoteShardOps; TruncationMode truncateOps = TruncationMode::kNoTruncation; @@ -195,6 +200,15 @@ intrusive_ptr<DocumentSource> DocumentSourceCurrentOp::createFromBson( elem.type() == BSONType::Bool); includeIdleConnections = (elem.boolean() ? ConnMode::kIncludeIdle : ConnMode::kExcludeIdle); + } else if (fieldName == kIdleSessionsFieldName) { + uassert( + ErrorCodes::FailedToParse, + str::stream() << "The 'idleSessions' parameter of the $currentOp stage must be a " + "boolean value, but found: " + << typeName(elem.type()), + elem.type() == BSONType::Bool); + includeIdleSessions = + (elem.boolean() ? SessionMode::kIncludeIdle : SessionMode::kExcludeIdle); } else if (fieldName == kAllUsersFieldName) { uassert(ErrorCodes::FailedToParse, str::stream() << "The 'allUsers' parameter of the $currentOp stage must be a " @@ -226,18 +240,27 @@ intrusive_ptr<DocumentSource> DocumentSourceCurrentOp::createFromBson( } } - return new DocumentSourceCurrentOp( - pExpCtx, includeIdleConnections, includeOpsFromAllUsers, showLocalOpsOnMongoS, truncateOps); + return new DocumentSourceCurrentOp(pExpCtx, + includeIdleConnections, + includeIdleSessions, + includeOpsFromAllUsers, + showLocalOpsOnMongoS, + truncateOps); } intrusive_ptr<DocumentSourceCurrentOp> DocumentSourceCurrentOp::create( const boost::intrusive_ptr<ExpressionContext>& pExpCtx, ConnMode includeIdleConnections, + SessionMode includeIdleSessions, UserMode includeOpsFromAllUsers, LocalOpsMode showLocalOpsOnMongoS, TruncationMode truncateOps) { - return new DocumentSourceCurrentOp( - pExpCtx, includeIdleConnections, includeOpsFromAllUsers, showLocalOpsOnMongoS, truncateOps); + return new DocumentSourceCurrentOp(pExpCtx, + includeIdleConnections, + includeIdleSessions, + includeOpsFromAllUsers, + showLocalOpsOnMongoS, + truncateOps); } Value DocumentSourceCurrentOp::serialize(boost::optional<ExplainOptions::Verbosity> explain) const { @@ -245,6 +268,8 @@ Value DocumentSourceCurrentOp::serialize(boost::optional<ExplainOptions::Verbosi {getSourceName(), Document{{kIdleConnectionsFieldName, _includeIdleConnections == ConnMode::kIncludeIdle ? Value(true) : Value()}, + {kIdleSessionsFieldName, + _includeIdleSessions == SessionMode::kExcludeIdle ? Value(false) : Value()}, {kAllUsersFieldName, _includeOpsFromAllUsers == UserMode::kIncludeAll ? Value(true) : Value()}, {kLocalOpsFieldName, diff --git a/src/mongo/db/pipeline/document_source_current_op.h b/src/mongo/db/pipeline/document_source_current_op.h index c1f3e4654c1..0533ea09418 100644 --- a/src/mongo/db/pipeline/document_source_current_op.h +++ b/src/mongo/db/pipeline/document_source_current_op.h @@ -37,6 +37,7 @@ public: using TruncationMode = MongoProcessInterface::CurrentOpTruncateMode; using ConnMode = MongoProcessInterface::CurrentOpConnectionsMode; using LocalOpsMode = MongoProcessInterface::CurrentOpLocalOpsMode; + using SessionMode = MongoProcessInterface::CurrentOpSessionsMode; using UserMode = MongoProcessInterface::CurrentOpUserMode; static constexpr StringData kStageName = "$currentOp"_sd; @@ -95,6 +96,7 @@ public: static boost::intrusive_ptr<DocumentSourceCurrentOp> create( const boost::intrusive_ptr<ExpressionContext>& pExpCtx, ConnMode includeIdleConnections = ConnMode::kExcludeIdle, + SessionMode includeIdleSessions = SessionMode::kIncludeIdle, UserMode includeOpsFromAllUsers = UserMode::kExcludeOthers, LocalOpsMode showLocalOpsOnMongoS = LocalOpsMode::kRemoteShardOps, TruncationMode truncateOps = TruncationMode::kNoTruncation); @@ -125,17 +127,20 @@ public: private: DocumentSourceCurrentOp(const boost::intrusive_ptr<ExpressionContext>& pExpCtx, - ConnMode includeIdleConnections = ConnMode::kExcludeIdle, - UserMode includeOpsFromAllUsers = UserMode::kExcludeOthers, - LocalOpsMode showLocalOpsOnMongoS = LocalOpsMode::kRemoteShardOps, - TruncationMode truncateOps = TruncationMode::kNoTruncation) + ConnMode includeIdleConnections, + SessionMode includeIdleSessions, + UserMode includeOpsFromAllUsers, + LocalOpsMode showLocalOpsOnMongoS, + TruncationMode truncateOps) : DocumentSource(pExpCtx), _includeIdleConnections(includeIdleConnections), + _includeIdleSessions(includeIdleSessions), _includeOpsFromAllUsers(includeOpsFromAllUsers), _showLocalOpsOnMongoS(showLocalOpsOnMongoS), _truncateOps(truncateOps) {} ConnMode _includeIdleConnections = ConnMode::kExcludeIdle; + SessionMode _includeIdleSessions = SessionMode::kIncludeIdle; UserMode _includeOpsFromAllUsers = UserMode::kExcludeOthers; LocalOpsMode _showLocalOpsOnMongoS = LocalOpsMode::kRemoteShardOps; TruncationMode _truncateOps = TruncationMode::kNoTruncation; diff --git a/src/mongo/db/pipeline/document_source_current_op_test.cpp b/src/mongo/db/pipeline/document_source_current_op_test.cpp index 127dd1f4ee8..c98c80f77eb 100644 --- a/src/mongo/db/pipeline/document_source_current_op_test.cpp +++ b/src/mongo/db/pipeline/document_source_current_op_test.cpp @@ -67,6 +67,7 @@ public: std::vector<BSONObj> getCurrentOps(OperationContext* opCtx, CurrentOpConnectionsMode connMode, + CurrentOpSessionsMode sessionMode, CurrentOpUserMode userMode, CurrentOpTruncateMode truncateMode) const { return _ops; diff --git a/src/mongo/db/pipeline/mongo_process_common.cpp b/src/mongo/db/pipeline/mongo_process_common.cpp index 634f9c71231..ff84540cd01 100644 --- a/src/mongo/db/pipeline/mongo_process_common.cpp +++ b/src/mongo/db/pipeline/mongo_process_common.cpp @@ -40,6 +40,7 @@ namespace mongo { std::vector<BSONObj> MongoProcessCommon::getCurrentOps(OperationContext* opCtx, CurrentOpConnectionsMode connMode, + CurrentOpSessionsMode sessionMode, CurrentOpUserMode userMode, CurrentOpTruncateMode truncateMode) const { AuthorizationSession* ctxAuth = AuthorizationSession::get(opCtx->getClient()); @@ -68,6 +69,11 @@ std::vector<BSONObj> MongoProcessCommon::getCurrentOps(OperationContext* opCtx, ops.emplace_back(_reportCurrentOpForClient(opCtx, client, truncateMode)); } + // If we need to report on idle Sessions, defer to the mongoD or mongoS implementations. + if (sessionMode == CurrentOpSessionsMode::kIncludeIdle) { + _reportCurrentOpsForIdleSessions(opCtx, userMode, &ops); + } + return ops; } diff --git a/src/mongo/db/pipeline/mongo_process_common.h b/src/mongo/db/pipeline/mongo_process_common.h index 19596ade67a..2fcdf1e54d4 100644 --- a/src/mongo/db/pipeline/mongo_process_common.h +++ b/src/mongo/db/pipeline/mongo_process_common.h @@ -45,6 +45,7 @@ public: std::vector<BSONObj> getCurrentOps(OperationContext* opCtx, CurrentOpConnectionsMode connMode, + CurrentOpSessionsMode sessionMode, CurrentOpUserMode userMode, CurrentOpTruncateMode) const final; @@ -57,6 +58,14 @@ protected: virtual BSONObj _reportCurrentOpForClient(OperationContext* opCtx, Client* client, CurrentOpTruncateMode truncateOps) const = 0; + + /** + * Iterates through all entries in the local SessionCatalog, and adds an entry to the 'ops' + * vector for each idle session that has stashed its transaction locks while sleeping. + */ + virtual void _reportCurrentOpsForIdleSessions(OperationContext* opCtx, + CurrentOpUserMode userMode, + std::vector<BSONObj>* ops) const = 0; }; } // namespace mongo diff --git a/src/mongo/db/pipeline/mongo_process_interface.h b/src/mongo/db/pipeline/mongo_process_interface.h index cdfae197c1f..3f63f7e66ff 100644 --- a/src/mongo/db/pipeline/mongo_process_interface.h +++ b/src/mongo/db/pipeline/mongo_process_interface.h @@ -63,6 +63,7 @@ public: enum class CurrentOpUserMode { kIncludeAll, kExcludeOthers }; enum class CurrentOpTruncateMode { kNoTruncation, kTruncateOps }; enum class CurrentOpLocalOpsMode { kLocalMongosOps, kRemoteShardOps }; + enum class CurrentOpSessionsMode { kIncludeIdle, kExcludeIdle }; struct MakePipelineOptions { MakePipelineOptions(){}; @@ -172,6 +173,7 @@ public: */ virtual std::vector<BSONObj> getCurrentOps(OperationContext* opCtx, CurrentOpConnectionsMode connMode, + CurrentOpSessionsMode sessionMode, CurrentOpUserMode userMode, CurrentOpTruncateMode) const = 0; diff --git a/src/mongo/db/pipeline/pipeline_d.cpp b/src/mongo/db/pipeline/pipeline_d.cpp index 9b4d7f6904c..2aa153f847d 100644 --- a/src/mongo/db/pipeline/pipeline_d.cpp +++ b/src/mongo/db/pipeline/pipeline_d.cpp @@ -51,6 +51,7 @@ #include "mongo/db/exec/shard_filter.h" #include "mongo/db/exec/working_set.h" #include "mongo/db/index/index_access_method.h" +#include "mongo/db/kill_sessions.h" #include "mongo/db/matcher/extensions_callback_real.h" #include "mongo/db/namespace_string.h" #include "mongo/db/pipeline/document_source.h" @@ -72,6 +73,7 @@ #include "mongo/db/s/metadata_manager.h" #include "mongo/db/s/sharding_state.h" #include "mongo/db/service_context.h" +#include "mongo/db/session_catalog.h" #include "mongo/db/stats/fill_locker_info.h" #include "mongo/db/stats/storage_stats.h" #include "mongo/db/stats/top.h" @@ -823,14 +825,37 @@ BSONObj PipelineD::MongoDInterface::_reportCurrentOpForClient( // Append lock stats before returning. if (auto clientOpCtx = client->getOperationContext()) { - Locker::LockerInfo lockerInfo; - clientOpCtx->lockState()->getLockerInfo(&lockerInfo); - fillLockerInfo(lockerInfo, builder); + if (auto lockerInfo = clientOpCtx->lockState()->getLockerInfo()) { + fillLockerInfo(*lockerInfo, builder); + } } return builder.obj(); } +void PipelineD::MongoDInterface::_reportCurrentOpsForIdleSessions(OperationContext* opCtx, + CurrentOpUserMode userMode, + std::vector<BSONObj>* ops) const { + auto sessionCatalog = SessionCatalog::get(opCtx); + + // If the user is listing only their own ops, we use makeSessionFilterForAuthenticatedUsers to + // create a pattern that will match against all authenticated usernames for the current client. + // If the user is listing ops for all users, we create an empty pattern; constructing an + // instance of SessionKiller::Matcher with this empty pattern will return all sessions. + auto sessionFilter = (userMode == CurrentOpUserMode::kExcludeOthers + ? makeSessionFilterForAuthenticatedUsers(opCtx) + : KillAllSessionsByPatternSet{{}}); + + sessionCatalog->scanSessions(opCtx, + {std::move(sessionFilter)}, + [&](OperationContext* opCtx, Session* session) { + auto op = session->reportStashedState(); + if (!op.isEmpty()) { + ops->emplace_back(op); + } + }); +} + std::unique_ptr<CollatorInterface> PipelineD::MongoDInterface::_getCollectionDefaultCollator( OperationContext* opCtx, StringData dbName, UUID collectionUUID) { auto it = _collatorCache.find(collectionUUID); diff --git a/src/mongo/db/pipeline/pipeline_d.h b/src/mongo/db/pipeline/pipeline_d.h index 630644413df..e9ab4fef8d2 100644 --- a/src/mongo/db/pipeline/pipeline_d.h +++ b/src/mongo/db/pipeline/pipeline_d.h @@ -115,6 +115,10 @@ public: Client* client, CurrentOpTruncateMode truncateOps) const final; + void _reportCurrentOpsForIdleSessions(OperationContext* opCtx, + CurrentOpUserMode userMode, + std::vector<BSONObj>* ops) const final; + private: /** * Looks up the collection default collator for the collection given by 'collectionUUID'. A diff --git a/src/mongo/db/pipeline/stub_mongo_process_interface.h b/src/mongo/db/pipeline/stub_mongo_process_interface.h index 0ec37b15eb4..9df0bb7baba 100644 --- a/src/mongo/db/pipeline/stub_mongo_process_interface.h +++ b/src/mongo/db/pipeline/stub_mongo_process_interface.h @@ -115,6 +115,7 @@ public: std::vector<BSONObj> getCurrentOps(OperationContext* opCtx, CurrentOpConnectionsMode connMode, + CurrentOpSessionsMode sessionMode, CurrentOpUserMode userMode, CurrentOpTruncateMode truncateMode) const override { MONGO_UNREACHABLE; diff --git a/src/mongo/db/session.cpp b/src/mongo/db/session.cpp index 30bb3dfd51c..adfd71c0421 100644 --- a/src/mongo/db/session.cpp +++ b/src/mongo/db/session.cpp @@ -44,12 +44,14 @@ #include "mongo/db/query/get_executor.h" #include "mongo/db/repl/read_concern_args.h" #include "mongo/db/retryable_writes_stats.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/sock.h" namespace mongo { namespace { @@ -805,6 +807,32 @@ void Session::_commitTransaction(stdx::unique_lock<stdx::mutex> lk, OperationCon _txnState = MultiDocumentTransactionState::kCommitted; } +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"); + { + BSONObjBuilder lsid(builder->subobjStart("lsid")); + getSessionId().serialize(&lsid); + } + builder->append("txnNumber", _activeTxnNumber); + builder->append("waitingForLock", false); + builder->append("active", false); + fillLockerInfo(*lockerInfo, *builder); + } + } +} + void Session::_checkValid(WithLock) const { uassert(ErrorCodes::ConflictingOperationInProgress, str::stream() << "Session " << getSessionId() diff --git a/src/mongo/db/session.h b/src/mongo/db/session.h index 95e48ddc7a6..099c3ec043e 100644 --- a/src/mongo/db/session.h +++ b/src/mongo/db/session.h @@ -77,6 +77,13 @@ public: 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); @@ -301,6 +308,18 @@ public: } /** + * 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; + + /** + * 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; + + /** * Scan through the list of operations and add new oplog entries for updating * config.transactions if needed. */ diff --git a/src/mongo/db/session_test.cpp b/src/mongo/db/session_test.cpp index 89b4cd773b3..c14900f7a11 100644 --- a/src/mongo/db/session_test.cpp +++ b/src/mongo/db/session_test.cpp @@ -38,9 +38,11 @@ #include "mongo/db/repl/optime.h" #include "mongo/db/service_context.h" #include "mongo/db/session_catalog.h" +#include "mongo/db/stats/fill_locker_info.h" #include "mongo/stdx/future.h" #include "mongo/stdx/memory.h" #include "mongo/unittest/unittest.h" +#include "mongo/util/net/sock.h" namespace mongo { namespace { @@ -622,6 +624,77 @@ TEST_F(SessionTest, StashAndUnstashResources) { session.commitTransaction(opCtx()); } +TEST_F(SessionTest, ReportStashedResources) { + 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); + + 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()); + ASSERT(opCtx()->getWriteUnitOfWork()); + + // Take a lock. This is expected in order to stash resources. + Lock::GlobalRead lk(opCtx(), Date_t::now()); + ASSERT(lk.isLocked()); + + // Build a BSONObj containing the details which we expect to see reported when we call + // Session::reportStashedState. + const auto lockerInfo = opCtx()->lockState()->getLockerInfo(); + ASSERT(lockerInfo); + + auto reportBuilder = + std::move(BSONObjBuilder() << "host" << getHostNameCachedAndPort() << "desc" + << "inactive transaction" + << "lsid" + << sessionId.toBSON() + << "txnNumber" + << txnNum + << "waitingForLock" + << false + << "active" + << false); + fillLockerInfo(*lockerInfo, reportBuilder); + + // Stash resources. The original Locker and RecoveryUnit now belong to the stash. + opCtx()->setStashedCursor(); + session.stashTransactionResources(opCtx()); + ASSERT(!opCtx()->getWriteUnitOfWork()); + + // Verify that the Session's report of its own stashed state aligns with our expectations. + ASSERT_BSONOBJ_EQ(session.reportStashedState(), reportBuilder.obj()); + + // 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()); + 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.commitTransaction(opCtx()); +} + TEST_F(SessionTest, StartTransactionRequiredToStartTxn) { const auto sessionId = makeLogicalSessionIdForTest(); Session session(sessionId); diff --git a/src/mongo/s/commands/pipeline_s.h b/src/mongo/s/commands/pipeline_s.h index c0955d491d1..dcbcda6a747 100644 --- a/src/mongo/s/commands/pipeline_s.h +++ b/src/mongo/s/commands/pipeline_s.h @@ -139,6 +139,13 @@ public: BSONObj _reportCurrentOpForClient(OperationContext* opCtx, Client* client, CurrentOpTruncateMode truncateOps) const final; + + void _reportCurrentOpsForIdleSessions(OperationContext* opCtx, + CurrentOpUserMode userMode, + std::vector<BSONObj>* ops) const final { + // This implementation is a no-op, since mongoS does not maintain a SessionCatalog or + // hold stashed locks for idle sessions. + } }; private: |