diff options
Diffstat (limited to 'src/mongo/db/session')
46 files changed, 10392 insertions, 0 deletions
diff --git a/src/mongo/db/session/SConscript b/src/mongo/db/session/SConscript new file mode 100644 index 00000000000..4e79131041f --- /dev/null +++ b/src/mongo/db/session/SConscript @@ -0,0 +1,179 @@ +# -*- mode: python -*- +Import("env") + +env = env.Clone() + +env.Library( + target='session_catalog', + source=[ + 'session_catalog.cpp', + ], + LIBDEPS=[ + 'kill_sessions', + 'logical_session_id', + 'logical_session_id_helpers', + ], + LIBDEPS_PRIVATE=[ + '$BUILD_DIR/mongo/db/internal_transactions_feature_flag', + '$BUILD_DIR/mongo/db/service_context', + ], +) + +env.Library( + target='logical_session_id', + source=[ + 'logical_session_id.cpp', + 'logical_session_id.idl', + ], + LIBDEPS=[ + '$BUILD_DIR/mongo/base', + '$BUILD_DIR/mongo/crypto/sha256_block', + '$BUILD_DIR/mongo/crypto/sha_block_${MONGO_CRYPTO}', + '$BUILD_DIR/mongo/idl/idl_parser', + '$BUILD_DIR/mongo/idl/server_parameter', + ], +) + +env.Library( + target='logical_session_id_helpers', + source=[ + 'internal_session_pool.cpp', + 'logical_session_id_helpers.cpp', + ], + LIBDEPS=[ + '$BUILD_DIR/mongo/db/auth/auth', + '$BUILD_DIR/mongo/db/auth/authprivilege', + 'logical_session_cache', + 'logical_session_id', + ], +) + +env.Library( + target='sessions_collection', + source=[ + 'sessions_collection.cpp', + ], + LIBDEPS=[ + '$BUILD_DIR/mongo/base', + '$BUILD_DIR/mongo/client/clientdriver_minimal', + '$BUILD_DIR/mongo/s/write_ops/batch_write_types', + 'logical_session_id', + 'logical_session_id_helpers', + ], + LIBDEPS_PRIVATE=[ + '$BUILD_DIR/mongo/db/index_commands_idl', + ], +) + +env.Library( + target='sessions_collection_mock', + source=[ + 'sessions_collection_mock.cpp', + ], + LIBDEPS=[ + '$BUILD_DIR/mongo/base', + 'logical_session_id', + 'sessions_collection', + ], +) + +env.Library( + target='sessions_collection_rs', + source=[ + 'sessions_collection_rs.cpp', + ], + LIBDEPS=[ + '$BUILD_DIR/mongo/base', + '$BUILD_DIR/mongo/client/authentication', + '$BUILD_DIR/mongo/client/remote_command_targeter', + '$BUILD_DIR/mongo/db/concurrency/lock_manager', + '$BUILD_DIR/mongo/db/dbdirectclient', + '$BUILD_DIR/mongo/db/repl/repl_coordinator_interface', + 'sessions_collection', + ], +) + +env.Library( + target='sessions_collection_standalone', + source=[ + 'sessions_collection_standalone.cpp', + ], + LIBDEPS=[ + '$BUILD_DIR/mongo/base', + '$BUILD_DIR/mongo/db/dbdirectclient', + 'logical_session_id', + 'sessions_collection', + ], +) + +env.Library( + target='logical_session_cache', + source=[ + 'logical_session_cache.cpp', + 'logical_session_cache_stats.idl', + 'logical_session_cache.idl', + ], + LIBDEPS=[ + '$BUILD_DIR/mongo/db/service_context', + 'logical_session_id', + ], + LIBDEPS_PRIVATE=[ + '$BUILD_DIR/mongo/idl/server_parameter', + ], +) + +env.Library( + target='logical_session_cache_impl', + source=[ + 'initialize_operation_session_info.cpp', + 'logical_session_cache_impl.cpp', + ], + LIBDEPS=[ + '$BUILD_DIR/mongo/db/service_liaison', + '$BUILD_DIR/mongo/db/session/kill_sessions', + 'logical_session_cache', + 'logical_session_id', + 'logical_session_id_helpers', + 'sessions_collection', + ], + LIBDEPS_PRIVATE=[ + '$BUILD_DIR/mongo/db/internal_transactions_feature_flag', + '$BUILD_DIR/mongo/db/s/sharding_api_d', + ], +) + +env.Library( + target='kill_sessions', + source=[ + 'kill_sessions.cpp', + 'kill_sessions_common.cpp', + 'session_killer.cpp', + 'kill_sessions.idl', + ], + LIBDEPS=[ + '$BUILD_DIR/mongo/base', + ], + LIBDEPS_PRIVATE=[ + '$BUILD_DIR/mongo/db/api_parameters', + '$BUILD_DIR/mongo/db/auth/auth', + '$BUILD_DIR/mongo/db/auth/authprivilege', + '$BUILD_DIR/mongo/idl/idl_parser', + '$BUILD_DIR/mongo/rpc/client_metadata', + 'logical_session_id_helpers', + ], +) + +env.Library( + target='kill_sessions_local', + source=[ + 'kill_sessions_local.cpp', + ], + LIBDEPS=[ + '$BUILD_DIR/mongo/db/query_exec', + 'kill_sessions', + ], + LIBDEPS_PRIVATE=[ + '$BUILD_DIR/mongo/db/transaction/transaction', + 'session_catalog', + ], +) diff --git a/src/mongo/db/session/initialize_operation_session_info.cpp b/src/mongo/db/session/initialize_operation_session_info.cpp new file mode 100644 index 00000000000..d1f58a25dcf --- /dev/null +++ b/src/mongo/db/session/initialize_operation_session_info.cpp @@ -0,0 +1,173 @@ +/** + * Copyright (C) 2018-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * 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 + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * 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 Server Side 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/session/initialize_operation_session_info.h" + +#include "mongo/db/auth/authorization_session.h" +#include "mongo/db/internal_transactions_feature_flag_gen.h" +#include "mongo/db/operation_context.h" +#include "mongo/db/session/logical_session_cache.h" +#include "mongo/db/session/logical_session_id_helpers.h" + +namespace mongo { + +OperationSessionInfoFromClient initializeOperationSessionInfo(OperationContext* opCtx, + const BSONObj& requestBody, + bool requiresAuth, + bool attachToOpCtx, + bool isReplSetMemberOrMongos) { + auto osi = OperationSessionInfoFromClient::parse(IDLParserContext{"OperationSessionInfo"}, + requestBody); + auto isAuthorizedForInternalClusterAction = + AuthorizationSession::get(opCtx->getClient()) + ->isAuthorizedForActionsOnResource(ResourcePattern::forClusterResource(), + ActionType::internal); + + if (opCtx->getClient()->isInDirectClient()) { + uassert(50891, + "Invalid to set operation session info in a direct client", + !osi.getSessionId() && !osi.getTxnNumber() && !osi.getAutocommit() && + !osi.getStartTransaction()); + } + + if (!requiresAuth) { + uassert(ErrorCodes::OperationNotSupportedInTransaction, + "This command is not supported in transactions", + !osi.getAutocommit()); + uassert( + 50889, "It is illegal to provide a txnNumber for this command", !osi.getTxnNumber()); + } + + if (auto authSession = AuthorizationSession::get(opCtx->getClient())) { + // If we're using the localhost bypass, and the client hasn't authenticated, + // logical sessions are disabled. A client may authenticate as the __sytem user, + // or as an externally authorized user. + if (authSession->isUsingLocalhostBypass() && !authSession->isAuthenticated()) { + return {}; + } + + // Do not initialize lsid when auth is enabled and no user is logged in since + // there is no sensible uid that can be assigned to it. + if (AuthorizationManager::get(opCtx->getServiceContext())->isAuthEnabled() && + !authSession->isAuthenticated() && !requiresAuth) { + return {}; + } + } + + if (osi.getSessionId()) { + stdx::lock_guard<Client> lk(*opCtx->getClient()); + + auto lsc = LogicalSessionCache::get(opCtx->getServiceContext()); + if (!lsc) { + // Ignore session information if the logical session cache has not been set up, e.g. on + // the embedded version of mongod. + return {}; + } + + // If osi lsid includes the uid, makeLogicalSessionId will also verify that the hash + // matches with the current user logged in. + auto lsid = makeLogicalSessionId(osi.getSessionId().value(), opCtx); + + if (!attachToOpCtx) { + return {}; + } + + if (isChildSession(lsid)) { + uassert(ErrorCodes::InvalidOptions, + "Internal sessions are only allowed for internal clients", + isAuthorizedForInternalClusterAction); + uassert(ErrorCodes::InvalidOptions, + "Internal sessions are not supported outside of transactions", + osi.getTxnNumber() && osi.getAutocommit() && !osi.getAutocommit().value()); + } + + opCtx->setLogicalSessionId(std::move(lsid)); + uassertStatusOK(lsc->vivify(opCtx, opCtx->getLogicalSessionId().value())); + } else { + uassert(ErrorCodes::InvalidOptions, + "Transaction number requires a session ID to also be specified", + !osi.getTxnNumber()); + } + + if (osi.getTxnNumber()) { + invariant(osi.getSessionId()); + stdx::lock_guard<Client> lk(*opCtx->getClient()); + + uassert(ErrorCodes::IllegalOperation, + "Transaction numbers are only allowed on a replica set member or mongos", + isReplSetMemberOrMongos); + uassert(ErrorCodes::InvalidOptions, + "Transaction number cannot be negative", + *osi.getTxnNumber() >= 0); + + opCtx->setTxnNumber(*osi.getTxnNumber()); + + if (auto txnRetryCounter = osi.getTxnRetryCounter()) { + uassert(ErrorCodes::InvalidOptions, + "txnRetryCounter is only allowed for internal clients", + isAuthorizedForInternalClusterAction); + uassert(ErrorCodes::InvalidOptions, + str::stream() << "Cannot specify txnRetryCounter for a retryable write", + osi.getAutocommit().has_value()); + uassert(ErrorCodes::InvalidOptions, + "txnRetryCounter cannot be negative", + txnRetryCounter >= 0); + opCtx->setTxnRetryCounter(*txnRetryCounter); + } + } else { + uassert(ErrorCodes::InvalidOptions, + "'autocommit' field requires a transaction number to also be specified", + !osi.getAutocommit()); + } + + if (osi.getAutocommit()) { + invariant(osi.getTxnNumber()); + uassert(ErrorCodes::InvalidOptions, + "Specifying autocommit=true is not allowed.", + !osi.getAutocommit().value()); + opCtx->setInMultiDocumentTransaction(); + } else { + uassert(ErrorCodes::InvalidOptions, + "'startTransaction' field requires 'autocommit' field to also be specified", + !osi.getStartTransaction()); + } + + if (osi.getStartTransaction()) { + invariant(osi.getAutocommit()); + uassert(ErrorCodes::InvalidOptions, + "Specifying startTransaction=false is not allowed.", + osi.getStartTransaction().value()); + } + + return osi; +} + +} // namespace mongo diff --git a/src/mongo/db/session/initialize_operation_session_info.h b/src/mongo/db/session/initialize_operation_session_info.h new file mode 100644 index 00000000000..b2c1fcde67d --- /dev/null +++ b/src/mongo/db/session/initialize_operation_session_info.h @@ -0,0 +1,58 @@ +/** + * Copyright (C) 2018-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * 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 + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * 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 Server Side 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 "mongo/db/session/logical_session_id.h" + +namespace mongo { + +/** + * Parses the session information from the body of a request and stores the sessionId and txnNumber + * on the current operation context. Must only be called once per operation and should be done right + * in the beginning. Note that the session info will be stored in the operation context and returned + * only if the current request supports it. For example, if attachToOpCtx is false or this is called + * within the context of DBDirectClient. + * + * Throws if the sessionId/txnNumber combination is not properly formatted. + * + * requiresAuth specifies if the command we're initializing operationSessionInfo for requires + * authorization or not. This can be determined by invoking ->requiresAuth() on the parsed command. + * If it does not require authorization, return boost::none. + * + * isReplSetMemberOrMongos needs to be true if the command contains a transaction number, otherwise + * this function will throw. + */ +OperationSessionInfoFromClient initializeOperationSessionInfo(OperationContext* opCtx, + const BSONObj& requestBody, + bool requiresAuth, + bool attachToOpCtx, + bool isReplSetMemberOrMongos); + +} // namespace mongo diff --git a/src/mongo/db/session/internal_session_pool.cpp b/src/mongo/db/session/internal_session_pool.cpp new file mode 100644 index 00000000000..98611a0bad2 --- /dev/null +++ b/src/mongo/db/session/internal_session_pool.cpp @@ -0,0 +1,198 @@ +/** + * Copyright (C) 2021-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * 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 + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * 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 Server Side 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/service_context.h" +#include "mongo/db/session/internal_session_pool.h" +#include "mongo/db/session/logical_session_cache.h" +#include "mongo/db/session/logical_session_id_helpers.h" +#include "mongo/logv2/log.h" + +#define MONGO_LOGV2_DEFAULT_COMPONENT ::mongo::logv2::LogComponent::kControl + + +namespace mongo { + +const auto serviceDecorator = ServiceContext::declareDecoration<InternalSessionPool>(); + +auto InternalSessionPool::get(ServiceContext* serviceContext) -> InternalSessionPool* { + return &serviceDecorator(serviceContext); +} + +auto InternalSessionPool::get(OperationContext* opCtx) -> InternalSessionPool* { + return get(opCtx->getServiceContext()); +} + +void InternalSessionPool::_reapExpiredSessions(WithLock) { + auto service = serviceDecorator.owner(this); + + for (auto it = _perUserSessionPool.begin(); it != _perUserSessionPool.end();) { + auto& userSessionPool = it->second; + while (!userSessionPool.empty() && + userSessionPool.back()._isExpired(service->getFastClockSource()->now())) { + userSessionPool.pop_back(); + } + + if (userSessionPool.empty()) { + it = _perUserSessionPool.erase(it, std::next(it)); + } else { + ++it; + } + } +} + +boost::optional<InternalSessionPool::Session> InternalSessionPool::_acquireSession( + SHA256Block userDigest, WithLock) { + if (!_perUserSessionPool.contains(userDigest)) { + _perUserSessionPool.try_emplace(userDigest, std::list<InternalSessionPool::Session>{}); + } + + auto& userSessionPool = _perUserSessionPool.at(userDigest); + + if (!userSessionPool.empty()) { + auto session = std::move(userSessionPool.front()); + userSessionPool.pop_front(); + + // Check if most recent session has expired before checking it out. + auto service = serviceDecorator.owner(this); + if (!session._isExpired(service->getFastClockSource()->now())) { + return session; + } + + // Most recent available session is expired, so all sessions must be expired. Clear + // list. + userSessionPool.clear(); + _perUserSessionPool.erase(userDigest); + } + + return boost::none; +} + +InternalSessionPool::Session InternalSessionPool::acquireSystemSession() { + const InternalSessionPool::Session session = [&] { + stdx::lock_guard<Latch> lock(_mutex); + + const auto& systemSession = makeSystemLogicalSessionId(); + auto session = _acquireSession(systemSession.getUid(), lock); + return session ? *session : InternalSessionPool::Session(systemSession, TxnNumber(0)); + }(); + + LOGV2_DEBUG(5876603, + 2, + "Acquired standalone internal session for system", + "lsid"_attr = session.getSessionId(), + "txnNumber"_attr = session.getTxnNumber()); + + return session; +} + +InternalSessionPool::Session InternalSessionPool::acquireStandaloneSession( + OperationContext* opCtx) { + const InternalSessionPool::Session session = [&] { + stdx::lock_guard<Latch> lock(_mutex); + + const auto& userDigest = getLogicalSessionUserDigestForLoggedInUser(opCtx); + auto session = _acquireSession(userDigest, lock); + return session ? *session + : InternalSessionPool::Session(makeLogicalSessionId(opCtx), TxnNumber(0)); + }(); + + LOGV2_DEBUG(5876600, + 2, + "Acquired standalone internal session for logged-in user", + "lsid"_attr = session.getSessionId(), + "txnNumber"_attr = session.getTxnNumber()); + + return session; +} + +InternalSessionPool::Session InternalSessionPool::acquireChildSession( + OperationContext* opCtx, const LogicalSessionId& parentLsid) { + const InternalSessionPool::Session session = [&] { + stdx::lock_guard<Latch> lock(_mutex); + + auto it = _childSessions.find(parentLsid); + if (it != _childSessions.end()) { + auto session = std::move(it->second); + invariant(parentLsid == getParentSessionId(session.getSessionId())); + _childSessions.erase(it); + return session; + } else { + auto lsid = makeLogicalSessionIdWithTxnUUID(parentLsid); + auto session = InternalSessionPool::Session(lsid, TxnNumber(0)); + return session; + } + }(); + + LOGV2_DEBUG(5876601, + 2, + "Acquired internal session with parent session", + "lsid"_attr = session.getSessionId(), + "txnNumber"_attr = session.getTxnNumber(), + "parentLsid"_attr = parentLsid); + + return session; +} + +void InternalSessionPool::release(Session session) { + LOGV2_DEBUG(5876602, + 2, + "Released internal session", + "lsid"_attr = session.getSessionId(), + "txnNumber"_attr = session.getTxnNumber()); + + ++session._txnNumber; + const auto& lsid = session.getSessionId(); + if (lsid.getTxnUUID()) { + stdx::lock_guard<Latch> lock(_mutex); + _childSessions.insert({*getParentSessionId(lsid), std::move(session)}); + } else { + stdx::lock_guard<Latch> lock(_mutex); + + if (!_perUserSessionPool.contains(lsid.getUid())) { + _perUserSessionPool.try_emplace(lsid.getUid(), + std::list<InternalSessionPool::Session>{}); + } + auto& userSessionPool = _perUserSessionPool.at(lsid.getUid()); + auto service = serviceDecorator.owner(this); + session._lastSeen = service->getFastClockSource()->now(); + userSessionPool.push_front(std::move(session)); + + // Clean up session pool by reaping all expired sessions. + _reapExpiredSessions(lock); + } +} + +std::size_t InternalSessionPool::numSessionsForUser_forTest(SHA256Block userDigest) { + return _perUserSessionPool[userDigest].size(); +} + +} // namespace mongo diff --git a/src/mongo/db/session/internal_session_pool.h b/src/mongo/db/session/internal_session_pool.h new file mode 100644 index 00000000000..a67cc2eb60a --- /dev/null +++ b/src/mongo/db/session/internal_session_pool.h @@ -0,0 +1,120 @@ +/** + * Copyright (C) 2021-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * 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 + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * 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 Server Side 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 "mongo/db/auth/authorization_manager.h" +#include "mongo/db/operation_context.h" +#include "mongo/stdx/unordered_map.h" + +#include <stack> + +namespace mongo { + +/** + * The InternalSessionPool creates a pool of reusable sessions, as needed, that can be used + * to execute an internal transaction. Sessions are reaped from the pool if they have not been used + * for over 15 minutes. The session pool is partitioned by uid. + */ +class InternalSessionPool { + +public: + class Session { + friend class InternalSessionPool; + + public: + Session(LogicalSessionId lsid, TxnNumber txnNumber) + : _lsid(std::move(lsid)), _txnNumber(txnNumber) {} + + const LogicalSessionId& getSessionId() const { + return _lsid; + } + const TxnNumber& getTxnNumber() const { + return _txnNumber; + } + + private: + bool _isExpired(Date_t now) { + auto timeElapsed = now - _lastSeen; + return timeElapsed > Minutes(localLogicalSessionTimeoutMinutes / 2); + } + + LogicalSessionId _lsid; + TxnNumber _txnNumber; + Date_t _lastSeen; + }; + + InternalSessionPool() = default; + + static InternalSessionPool* get(ServiceContext* serviceContext); + static InternalSessionPool* get(OperationContext* opCtx); + + /** + * Use this method to acquire an internal session for a system initiated transaction. + */ + Session acquireSystemSession(); + + /** + * Use this method to acquire an internal session when the user is not currently running a + * session. + */ + Session acquireStandaloneSession(OperationContext* opCtx); + + /** + * Use this method to acquire an internal session nested in a client session. + */ + Session acquireChildSession(OperationContext* opCtx, const LogicalSessionId& parentLsid); + + /** + * Use this method to release all types of acquired sessions back to the session pool. Upon + * release, all expired sessions are removed from the session pool. + */ + void release(Session session); + + /** + * Use this method to confirm the size of list at _userSessionPool[userDigest]. To be used for + * testing only. + */ + std::size_t numSessionsForUser_forTest(SHA256Block userDigest); + +private: + void _reapExpiredSessions(WithLock); + boost::optional<InternalSessionPool::Session> _acquireSession(SHA256Block userDigest, WithLock); + + // Used for associating parent lsids with existing Sessions of the form <id, uid, txnUUID>. + LogicalSessionIdMap<Session> _childSessions; + + // Map partitioning the session pool by logged in user. + stdx::unordered_map<SHA256Block, std::list<Session>, SHA256Block::Hash> _perUserSessionPool; + + // Protects the internal data structures. + mutable Mutex _mutex = MONGO_MAKE_LATCH("InternalSessionPool::_mutex"); +}; + +} // namespace mongo diff --git a/src/mongo/db/session/internal_session_pool_test.cpp b/src/mongo/db/session/internal_session_pool_test.cpp new file mode 100644 index 00000000000..6a1603922d2 --- /dev/null +++ b/src/mongo/db/session/internal_session_pool_test.cpp @@ -0,0 +1,282 @@ +/** + * Copyright (C) 2021-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * 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 + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * 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 Server Side 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/service_context_test_fixture.h" +#include "mongo/db/service_liaison_mock.h" +#include "mongo/db/session/internal_session_pool.h" +#include "mongo/db/session/logical_session_cache.h" +#include "mongo/db/session/logical_session_cache_impl.h" +#include "mongo/db/session/logical_session_id_helpers.h" +#include "mongo/db/session/sessions_collection_mock.h" +#include "mongo/idl/server_parameter_test_util.h" +#include "mongo/unittest/unittest.h" +#include "mongo/util/clock_source_mock.h" + +namespace mongo { +namespace { + +class InternalSessionPoolTest : public ServiceContextTest { +public: + void setUp() override { + ServiceContextTest::setUp(); + serverGlobalParams.clusterRole = ClusterRole::ShardServer; + _pool = InternalSessionPool::get(getServiceContext()); + _opCtx = makeOperationContext(); + } + + OperationContext* opCtx() const { + return _opCtx.get(); + } + +protected: + InternalSessionPool* _pool; + +private: + ServiceContext::UniqueOperationContext _opCtx; +}; + +TEST_F(InternalSessionPoolTest, AcquireStandaloneSessionFromEmptyPool) { + auto session = _pool->acquireStandaloneSession(opCtx()); + ASSERT_EQ(TxnNumber(0), session.getTxnNumber()); +} + +TEST_F(InternalSessionPoolTest, AcquireWithParentSessionFromEmptyPool) { + auto parentLsid = makeLogicalSessionIdForTest(); + auto session = _pool->acquireChildSession(opCtx(), parentLsid); + + ASSERT_EQ(parentLsid, *getParentSessionId(session.getSessionId())); + ASSERT_EQ(TxnNumber(0), session.getTxnNumber()); +} + +TEST_F(InternalSessionPoolTest, AcquireStandaloneSessionFromPool) { + auto expectedLsid = makeLogicalSessionIdForTest(); + auto sessionToRelease = InternalSessionPool::Session(expectedLsid, TxnNumber(0)); + _pool->release(sessionToRelease); + + auto session = _pool->acquireStandaloneSession(opCtx()); + + ASSERT_EQ(expectedLsid, session.getSessionId()); + // txnNumber should be 1 larger than the released session. + ASSERT_EQ(TxnNumber(1), session.getTxnNumber()); +} + +TEST_F(InternalSessionPoolTest, AcquireWithParentSessionFromPoolWithoutParentEntry) { + LogicalSessionId parentLsid1 = makeLogicalSessionIdForTest(); + LogicalSessionId parentLsid2 = makeLogicalSessionIdForTest(); + + auto parentSession1 = InternalSessionPool::Session(parentLsid1, TxnNumber(1)); + _pool->release(parentSession1); + + auto session = _pool->acquireChildSession(opCtx(), parentLsid2); + + ASSERT_NOT_EQUALS(parentLsid1, session.getSessionId()); + ASSERT_EQ(TxnNumber(0), session.getTxnNumber()); +} + +TEST_F(InternalSessionPoolTest, AcquireChildForSameParentWithoutIntermediateRelease) { + LogicalSessionId parentLsid = makeLogicalSessionIdForTest(); + + auto childLsid1 = makeLogicalSessionIdWithTxnUUID(parentLsid); + _pool->release(InternalSessionPool::Session(childLsid1, TxnNumber(0))); + + auto childSession1 = _pool->acquireChildSession(opCtx(), parentLsid); + + ASSERT_NOT_EQUALS(parentLsid, childSession1.getSessionId()); + ASSERT_EQ(childLsid1, childSession1.getSessionId()); + ASSERT_EQ(TxnNumber(1), childSession1.getTxnNumber()); + + // If we acquire a child for the same parent without first releasing the checked out child, it + // shouldn't block or prevent future child session reuse. + + auto childSession2 = _pool->acquireChildSession(opCtx(), parentLsid); + ASSERT_NOT_EQUALS(parentLsid, childSession2.getSessionId()); + ASSERT_NOT_EQUALS(childLsid1, childSession2.getSessionId()); + ASSERT_EQ(TxnNumber(0), childSession2.getTxnNumber()); + + _pool->release(childSession2); + + auto childSession3 = _pool->acquireChildSession(opCtx(), parentLsid); + ASSERT_EQ(childSession2.getSessionId(), childSession3.getSessionId()); + ASSERT_EQ(TxnNumber(1), childSession3.getTxnNumber()); + + // Releasing the first child session back into the pool should overwrite the previous session + // and still allow for reuse. + + _pool->release(childSession1); + + auto childSession4 = _pool->acquireChildSession(opCtx(), parentLsid); + ASSERT_EQ(childSession1.getSessionId(), childSession4.getSessionId()); + ASSERT_EQ(TxnNumber(2), childSession4.getTxnNumber()); +} + +TEST_F(InternalSessionPoolTest, AcquireWithParentSessionFromPoolWithParentEntry) { + LogicalSessionId parentLsid1 = makeLogicalSessionIdForTest(); + LogicalSessionId parentLsid2 = makeLogicalSessionIdForTest(); + + // Create child sessions for each parent session and release them in the pool. + auto childLsid1 = makeLogicalSessionIdWithTxnUUID(parentLsid1); + auto childLsid2 = makeLogicalSessionIdWithTxnUUID(parentLsid2); + + _pool->release(InternalSessionPool::Session(childLsid1, TxnNumber(1))); + _pool->release(InternalSessionPool::Session(childLsid2, TxnNumber(2))); + + auto childSession2 = _pool->acquireChildSession(opCtx(), parentLsid2); + + ASSERT_NOT_EQUALS(parentLsid1, childSession2.getSessionId()); + ASSERT_NOT_EQUALS(childLsid1, childSession2.getSessionId()); + ASSERT_NOT_EQUALS(parentLsid2, childSession2.getSessionId()); + ASSERT_EQ(childLsid2, childSession2.getSessionId()); + + // txnNumber should be 1 larger than the released parent session. + ASSERT_EQ(TxnNumber(3), childSession2.getTxnNumber()); +} + +TEST_F(InternalSessionPoolTest, ReuseUnexpiredStandaloneSessionForSystemUser) { + auto expectedLsid = makeSystemLogicalSessionId(); + auto sessionToRelease = InternalSessionPool::Session(expectedLsid, TxnNumber(0)); + _pool->release(sessionToRelease); + + auto session = _pool->acquireSystemSession(); + ASSERT_EQ(expectedLsid, session.getSessionId()); + + // txnNumber should be 1 larger than the released session. + ASSERT_EQ(TxnNumber(1), session.getTxnNumber()); +} + +TEST_F(InternalSessionPoolTest, ReuseUnexpiredStandaloneSession) { + auto session = _pool->acquireStandaloneSession(opCtx()); + auto expectedLsid = session.getSessionId(); + _pool->release(session); + + auto acquiredSession = _pool->acquireStandaloneSession(opCtx()); + ASSERT_EQ(expectedLsid, session.getSessionId()); + + // txnNumber should be 1 larger than the released session. + ASSERT_EQ(TxnNumber(1), acquiredSession.getTxnNumber()); +} + +TEST_F(InternalSessionPoolTest, StandaloneSessionAcquiredInReverseOrder) { + auto expectedLsid1 = makeLogicalSessionId(opCtx()); + auto expectedLsid2 = makeLogicalSessionId(opCtx()); + auto sessionToRelease1 = InternalSessionPool::Session(expectedLsid1, TxnNumber(0)); + auto sessionToRelease2 = InternalSessionPool::Session(expectedLsid2, TxnNumber(0)); + _pool->release(sessionToRelease1); + _pool->release(sessionToRelease2); + + auto session = _pool->acquireStandaloneSession(opCtx()); + ASSERT_EQ(expectedLsid2, session.getSessionId()); + // txnNumber should be 1 larger than the released session. + ASSERT_EQ(TxnNumber(1), session.getTxnNumber()); + + auto nextSession = _pool->acquireStandaloneSession(opCtx()); + ASSERT_EQ(expectedLsid1, nextSession.getSessionId()); + // txnNumber should be 1 larger than the released session. + ASSERT_EQ(TxnNumber(1), nextSession.getTxnNumber()); +} + +TEST_F(InternalSessionPoolTest, UserCannotAcquireSystemStandaloneSession) { + auto lsid = makeLogicalSessionId(opCtx()); + auto sessionToRelease = InternalSessionPool::Session(lsid, TxnNumber(0)); + _pool->release(sessionToRelease); + + auto systemLsid = makeSystemLogicalSessionId(); + _pool->release(InternalSessionPool::Session(systemLsid, TxnNumber(0))); + + auto session = _pool->acquireStandaloneSession(opCtx()); + ASSERT_NE(systemLsid, session.getSessionId()); + ASSERT_EQ(lsid, session.getSessionId()); + // txnNumber should be 1 larger than the released session. + ASSERT_EQ(TxnNumber(1), session.getTxnNumber()); + _pool->release(session); + + auto nextSession = _pool->acquireSystemSession(); + ASSERT_NE(lsid, nextSession.getSessionId()); + ASSERT_EQ(systemLsid, nextSession.getSessionId()); + // txnNumber should be 1 larger than the released session. + ASSERT_EQ(TxnNumber(1), nextSession.getTxnNumber()); +} + +TEST_F(InternalSessionPoolTest, StaleStandaloneSessionIsNotAcquired) { + auto service = getServiceContext(); + const std::shared_ptr<ClockSourceMock> mockClock = std::make_shared<ClockSourceMock>(); + service->setFastClockSource(std::make_unique<SharedClockSourceAdapter>(mockClock)); + + auto expectedLsid = makeSystemLogicalSessionId(); + auto sessionToRelease = InternalSessionPool::Session(expectedLsid, TxnNumber(0)); + _pool->release(sessionToRelease); + mockClock->advance(Minutes{localLogicalSessionTimeoutMinutes}); + + auto acquiredSession = _pool->acquireSystemSession(); + ASSERT_NE(expectedLsid, acquiredSession.getSessionId()); + // Should be a new session. + ASSERT_EQ(TxnNumber(0), acquiredSession.getTxnNumber()); +} + +TEST_F(InternalSessionPoolTest, ExpiredSessionReapedDuringRelease) { + auto service = getServiceContext(); + const std::shared_ptr<ClockSourceMock> mockClock = std::make_shared<ClockSourceMock>(); + service->setFastClockSource(std::make_unique<SharedClockSourceAdapter>(mockClock)); + + auto lsid0 = makeSystemLogicalSessionId(); + auto session0 = InternalSessionPool::Session(lsid0, TxnNumber(0)); + _pool->release(session0); + + auto lsid1 = makeSystemLogicalSessionId(); + auto session1 = InternalSessionPool::Session(lsid1, TxnNumber(0)); + mockClock->advance(Minutes{localLogicalSessionTimeoutMinutes}); + _pool->release(session1); + + // Both system sessions have the same userDigest, but the session with lsid0 should've been + // removed during release. + ASSERT_EQ(1, _pool->numSessionsForUser_forTest(lsid1.getUid())); +} + +TEST_F(InternalSessionPoolTest, UserExpiredSessionsReapedWhenAnotherUserReleasesSession) { + auto service = getServiceContext(); + const std::shared_ptr<ClockSourceMock> mockClock = std::make_shared<ClockSourceMock>(); + service->setFastClockSource(std::make_unique<SharedClockSourceAdapter>(mockClock)); + + auto lsid0 = makeLogicalSessionId(opCtx()); + auto sessionToRelease = InternalSessionPool::Session(lsid0, TxnNumber(0)); + _pool->release(sessionToRelease); + + auto lsid1 = makeSystemLogicalSessionId(); + auto session1 = InternalSessionPool::Session(lsid1, TxnNumber(0)); + mockClock->advance(Minutes{localLogicalSessionTimeoutMinutes}); + _pool->release(session1); + + // The session with lsid0 should've been removed from the session pool when lsid1 was released. + ASSERT_EQ(0, _pool->numSessionsForUser_forTest(lsid0.getUid())); + ASSERT_EQ(1, _pool->numSessionsForUser_forTest(lsid1.getUid())); +} + +} // namespace +} // namespace mongo diff --git a/src/mongo/db/session/kill_sessions.cpp b/src/mongo/db/session/kill_sessions.cpp new file mode 100644 index 00000000000..7626afa111b --- /dev/null +++ b/src/mongo/db/session/kill_sessions.cpp @@ -0,0 +1,141 @@ +/** + * Copyright (C) 2018-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * 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 + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * 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 Server Side 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/session/kill_sessions.h" + +#include "mongo/db/api_parameters.h" +#include "mongo/db/auth/authorization_session.h" +#include "mongo/db/client.h" +#include "mongo/db/operation_context.h" +#include "mongo/db/service_context.h" + +namespace mongo { + +namespace { + +std::vector<KillAllSessionsUser> getKillAllSessionsImpersonateUsers(OperationContext* opCtx) { + auto* as = AuthorizationSession::get(opCtx->getClient()); + + std::vector<KillAllSessionsUser> out; + + if (auto name = as->getAuthenticatedUserName()) { + out.emplace_back(); + out.back().setUser(name->getUser()); + out.back().setDb(name->getDB()); + } + + return out; +} + +std::vector<KillAllSessionsRole> getKillAllSessionsImpersonateRoles(OperationContext* opCtx) { + AuthorizationSession* authSession = AuthorizationSession::get(opCtx->getClient()); + + std::vector<KillAllSessionsRole> out; + + for (auto iter = authSession->getAuthenticatedRoleNames(); iter.more(); iter.next()) { + out.emplace_back(); + out.back().setRole(iter->getRole()); + out.back().setDb(iter->getDB()); + } + + return out; +} + +} // namespace + +std::tuple<boost::optional<UserName>, std::vector<RoleName>> +getKillAllSessionsByPatternImpersonateData(const KillAllSessionsByPattern& pattern) { + std::tuple<boost::optional<UserName>, std::vector<RoleName>> out; + + auto& user = std::get<0>(out); + auto& roles = std::get<1>(out); + + if (pattern.getUsers() && (pattern.getUsers()->size() > 0)) { + uassert(ErrorCodes::BadValue, + "Too many users in impersonation data", + pattern.getUsers()->size() <= 1); + const auto& impUser = pattern.getUsers().value()[0]; + user = UserName(impUser.getUser(), impUser.getDb()); + } + + if (pattern.getRoles()) { + roles.reserve(pattern.getRoles()->size()); + + for (auto&& role : pattern.getRoles().value()) { + roles.emplace_back(role.getRole(), role.getDb()); + } + } + + return out; +} + +KillAllSessionsByPatternItem makeKillAllSessionsByPattern(OperationContext* opCtx) { + KillAllSessionsByPattern kasbp; + + kasbp.setUsers(getKillAllSessionsImpersonateUsers(opCtx)); + kasbp.setRoles(getKillAllSessionsImpersonateRoles(opCtx)); + return {kasbp, APIParameters::get(opCtx)}; +} + +KillAllSessionsByPatternItem makeKillAllSessionsByPattern(OperationContext* opCtx, + const KillAllSessionsUser& kasu) { + KillAllSessionsByPatternItem item = makeKillAllSessionsByPattern(opCtx); + + auto authMgr = AuthorizationManager::get(opCtx->getServiceContext()); + + UserName un(kasu.getUser(), kasu.getDb()); + + auto user = uassertStatusOK(authMgr->acquireUser(opCtx, un)); + item.pattern.setUid(user->getDigest()); + return item; +} + +KillAllSessionsByPatternSet makeSessionFilterForAuthenticatedUsers(OperationContext* opCtx) { + auto* as = AuthorizationSession::get(opCtx->getClient()); + KillAllSessionsByPatternSet patterns; + + if (auto user = as->getAuthenticatedUser()) { + KillAllSessionsByPattern pattern; + pattern.setUid(user.value()->getDigest()); + KillAllSessionsByPatternItem item{std::move(pattern), APIParameters::get(opCtx)}; + patterns.emplace(std::move(item)); + } + return patterns; +} + +KillAllSessionsByPatternItem makeKillAllSessionsByPattern(OperationContext* opCtx, + const LogicalSessionId& lsid) { + auto item = makeKillAllSessionsByPattern(opCtx); + item.pattern.setLsid(lsid); + return item; +} + +} // namespace mongo diff --git a/src/mongo/db/session/kill_sessions.h b/src/mongo/db/session/kill_sessions.h new file mode 100644 index 00000000000..d4b29313bf2 --- /dev/null +++ b/src/mongo/db/session/kill_sessions.h @@ -0,0 +1,118 @@ +/** + * Copyright (C) 2018-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * 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 + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * 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 Server Side 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 <tuple> + +#include "mongo/db/api_parameters.h" +#include "mongo/db/auth/role_name.h" +#include "mongo/db/auth/user_name.h" +#include "mongo/db/session/kill_sessions_gen.h" +#include "mongo/db/session/logical_session_id.h" + +namespace mongo { + +class OperationContext; +class ServiceContext; + +struct KillAllSessionsByPatternItem { + KillAllSessionsByPattern pattern; + APIParameters apiParameters; +}; + +struct KillAllSessionsByPatternItemHash { + std::size_t operator()(const KillAllSessionsByPatternItem& item) const { + auto& pattern = item.pattern; + if (pattern.getLsid()) { + return lsidHasher(*pattern.getLsid()); + } else if (pattern.getUid()) { + return uidHasher(*pattern.getUid()); + } + + // fall through for killAll + return 0; + } + + LogicalSessionIdHash lsidHasher; + SHA256Block::Hash uidHasher; +}; + +/** + * Patterns are specifically equal if they differ only by impersonate data. + */ +inline bool operator==(const KillAllSessionsByPatternItem& lhs, + const KillAllSessionsByPatternItem& rhs) { + auto makeEqualityLens = [](const auto& item) { + return std::tie(item.pattern.getLsid(), item.pattern.getUid(), item.apiParameters); + }; + + return makeEqualityLens(lhs) == makeEqualityLens(rhs); +} + +inline bool operator!=(const KillAllSessionsByPatternItem& lhs, + const KillAllSessionsByPatternItem& rhs) { + return !(lhs == rhs); +} + +using KillAllSessionsByPatternSet = + stdx::unordered_set<KillAllSessionsByPatternItem, KillAllSessionsByPatternItemHash>; + +std::tuple<boost::optional<UserName>, std::vector<RoleName>> +getKillAllSessionsByPatternImpersonateData(const KillAllSessionsByPattern& pattern); + +/** + * Note: All three of the below makeKillAllSessionsByPattern helpers take opCtx to inline the + * required impersonate data + */ + +/** + * Constructs a kill sessions pattern which kills all sessions + */ +KillAllSessionsByPatternItem makeKillAllSessionsByPattern(OperationContext* opCtx); + +/** + * Constructs a kill sessions pattern for a particular user + */ +KillAllSessionsByPatternItem 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 + */ +KillAllSessionsByPatternItem makeKillAllSessionsByPattern(OperationContext* opCtx, + const LogicalSessionId& lsid); + +} // namespace mongo diff --git a/src/mongo/db/session/kill_sessions.idl b/src/mongo/db/session/kill_sessions.idl new file mode 100644 index 00000000000..6081f8b0cef --- /dev/null +++ b/src/mongo/db/session/kill_sessions.idl @@ -0,0 +1,98 @@ +# Copyright (C) 2018-present MongoDB, Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the Server Side Public License, version 1, +# as published by MongoDB, Inc. +# +# 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 +# Server Side Public License for more details. +# +# You should have received a copy of the Server Side Public License +# along with this program. If not, see +# <http://www.mongodb.com/licensing/server-side-public-license>. +# +# 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 Server Side 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. +# + +global: + cpp_namespace: "mongo" + cpp_includes: + - "mongo/db/session/kill_sessions_util.h" + +imports: + - "mongo/crypto/sha256_block.idl" + - "mongo/db/session/logical_session_id.idl" + - "mongo/idl/basic_types.idl" + +structs: + + KillSessionsCmdFromClient: + description: "A struct representing a killSessions command from a client" + strict: false + fields: + killSessions: array<LogicalSessionFromClient> + + KillAllSessionsUser: + description: "A struct representing a killAllSessions User" + strict: true + fields: + user: string + db: string + + KillAllSessionsRole: + description: "A struct representing a killAllSessions Role" + strict: true + fields: + role: string + db: string + + KillAllSessionsCmd: + description: "A struct representing a killAllSessions command" + strict: false + fields: + killAllSessions: array<KillAllSessionsUser> + + KillAllSessionsByPattern: + description: "A struct representing a killAllSessionsByPatternCmd kill pattern" + strict: true + fields: + lsid: + type: LogicalSessionId + optional: true + validator: + callback: "kill_sessions::validateLsid" + uid: + type: sha256Block + optional: true + users: + description: "logged in users for impersonate" + type: array<KillAllSessionsUser> + optional: true + roles: + description: "logged in roles for impersonate" + type: array<KillAllSessionsRole> + optional: true + + KillAllSessionsByPatternCmd: + description: "A struct representing a killAllSessionsByPattern command" + strict: false + fields: + killAllSessionsByPattern: array<KillAllSessionsByPattern> + + KillSessionsCmdToServer: + description: "A struct representing a killSessions command to a server" + strict: true + fields: + killSessions: array<LogicalSessionId> diff --git a/src/mongo/db/session/kill_sessions_common.cpp b/src/mongo/db/session/kill_sessions_common.cpp new file mode 100644 index 00000000000..235c7dd3000 --- /dev/null +++ b/src/mongo/db/session/kill_sessions_common.cpp @@ -0,0 +1,126 @@ +/** + * Copyright (C) 2018-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * 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 + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * 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 Server Side 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/session/kill_sessions_common.h" + +#include "mongo/db/client.h" +#include "mongo/db/operation_context.h" +#include "mongo/db/service_context.h" +#include "mongo/db/session/session_killer.h" +#include "mongo/logv2/log.h" +#include "mongo/rpc/metadata/client_metadata.h" + +#define MONGO_LOGV2_DEFAULT_COMPONENT ::mongo::logv2::LogComponent::kCommand + + +namespace mongo { + +SessionKiller::Result killSessionsLocalKillOps(OperationContext* opCtx, + const SessionKiller::Matcher& matcher) { + for (ServiceContext::LockedClientsCursor cursor(opCtx->getClient()->getServiceContext()); + Client* client = cursor.next();) { + invariant(client); + stdx::unique_lock<Client> lk(*client); + + OperationContext* opCtxToKill = client->getOperationContext(); + if (opCtxToKill) { + const auto& lsid = opCtxToKill->getLogicalSessionId(); + + if (lsid) { + if (const KillAllSessionsByPattern* pattern = matcher.match(*lsid)) { + ScopedKillAllSessionsByPatternImpersonator impersonator(opCtx, *pattern); + + LOGV2(20706, + "Killing op {opId} as part of killing session {lsid}", + "Killing op as part of killing session", + "opId"_attr = opCtxToKill->getOpID(), + "lsid"_attr = lsid->toBSON()); + + opCtx->getServiceContext()->killOperation(lk, opCtxToKill); + } + } + } + } + + return {std::vector<HostAndPort>{}}; +} + +Status killSessionsCmdHelper(OperationContext* opCtx, + BSONObjBuilder& result, + const KillAllSessionsByPatternSet& patterns) { + auto killResult = SessionKiller::get(opCtx)->kill(opCtx, patterns); + + if (!killResult->isOK()) { + return killResult->getStatus(); + } + + if (!killResult->getValue().empty()) { + BSONArrayBuilder bab(result.subarrayStart("failedHosts")); + for (const auto& host : killResult->getValue()) { + bab.append(host.toString()); + } + + return Status(ErrorCodes::HostUnreachable, "Failed to kill on some hosts"); + } + + return Status::OK(); +} + +void killSessionsReport(OperationContext* opCtx, const BSONObj& cmdObj) { + + logv2::DynamicAttributes attr; + + auto client = opCtx->getClient(); + if (client) { + if (AuthorizationManager::get(client->getServiceContext())->isAuthEnabled()) { + if (auto user = AuthorizationSession::get(client)->getAuthenticatedUserName()) { + attr.add("user", BSON_ARRAY(user->toBSON())); + } else { + attr.add("user", BSONArray()); + } + } + + if (client->session()) { + attr.add("remote", client->session()->remote()); + } + + if (auto metadata = ClientMetadata::get(client)) { + attr.add("metadata", metadata->getDocument()); + } + } + + attr.add("command", cmdObj); + + LOGV2(558701, "Success: kill session", attr); +} + +} // namespace mongo diff --git a/src/mongo/db/session/kill_sessions_common.h b/src/mongo/db/session/kill_sessions_common.h new file mode 100644 index 00000000000..3f0a8ddb06d --- /dev/null +++ b/src/mongo/db/session/kill_sessions_common.h @@ -0,0 +1,169 @@ +/** + * Copyright (C) 2018-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * 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 + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * 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 Server Side 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 "mongo/db/session/kill_sessions.h" + +#include <vector> + +#include "mongo/base/status.h" +#include "mongo/db/auth/authorization_session.h" +#include "mongo/db/operation_context.h" +#include "mongo/db/session/session_killer.h" +#include "mongo/stdx/unordered_set.h" +#include "mongo/util/str.h" + +namespace mongo { + +/** + * Local killing involves looping over all local operations, checking to see if they have matching + * logical session ids, and killing if they do. + */ +SessionKiller::Result killSessionsLocalKillOps(OperationContext* opCtx, + const SessionKiller::Matcher& matcher); + +/** + * Helper for executing a pattern set from a kill sessions style command. + */ +Status killSessionsCmdHelper(OperationContext* opCtx, + BSONObjBuilder& result, + const KillAllSessionsByPatternSet& patterns); + +/** + * Helper for logging kill sessions command. + */ +void killSessionsReport(OperationContext* opCtx, const BSONObj& cmdObj); + +class ScopedKillAllSessionsByPatternImpersonator { +public: + ScopedKillAllSessionsByPatternImpersonator(OperationContext* opCtx, + const KillAllSessionsByPattern& pattern) { + AuthorizationSession* authSession = AuthorizationSession::get(opCtx->getClient()); + + if (pattern.getUsers() && pattern.getRoles()) { + std::tie(_name, _roles) = getKillAllSessionsByPatternImpersonateData(pattern); + _raii.emplace(authSession, &_name, &_roles); + } + } + +private: + boost::optional<UserName> _name; + std::vector<RoleName> _roles; + boost::optional<AuthorizationSession::ScopedImpersonate> _raii; +}; + +/** + * This elaborate bit of artiface helps us to adapt the shape of a cursor manager that we know from + * logical sessions with the different ways to cancel cursors in mongos versus mongod. I.e. the + * two types share no code, but do share enough shape to re-use some boilerplate. + */ +template <typename Eraser> +class KillCursorsBySessionAdaptor { +public: + KillCursorsBySessionAdaptor(OperationContext* opCtx, + const SessionKiller::Matcher& matcher, + Eraser&& eraser) + : _opCtx(opCtx), _matcher(matcher), _cursorsKilled(0), _eraser(eraser) {} + + /** + * Kills cursors in 'mgr' which belong to a session matching the SessionKilled::Matcher with + * which this adaptor was constructed. + */ + template <typename Mgr> + void operator()(Mgr& mgr) noexcept { + LogicalSessionIdSet activeSessions; + mgr.appendActiveSessions(&activeSessions); + + for (const auto& session : activeSessions) { + if (const KillAllSessionsByPattern* pattern = _matcher.match(session)) { + ScopedKillAllSessionsByPatternImpersonator impersonator(_opCtx, *pattern); + + auto cursors = mgr.getCursorsForSession(session); + for (const auto& id : cursors) { + try { + _eraser(mgr, id); + _cursorsKilled++; + } catch (const ExceptionFor<ErrorCodes::CursorNotFound>&) { + // Cursor was killed separately after this command was initiated. Still + // count the cursor as killed here, since the user's request is + // technically satisfied. + _cursorsKilled++; + } catch (const DBException& ex) { + _failures.push_back(ex.toStatus()); + } + } + } + } + } + + /** + * Returns an OK status if no errors were encountered during cursor killing, or a non-OK status + * summarizing any errors encountered. + */ + Status getStatus() const { + if (_failures.empty()) { + return Status::OK(); + } + + if (_failures.size() == 1) { + return _failures.back(); + } + + return Status(_failures.back().code(), + str::stream() << "Encountered " << _failures.size() + << " errors while killing cursors, " + "showing most recent error: " + << _failures.back().reason()); + } + + /** + * Returns the number of cursors killed by operator(). + */ + int getCursorsKilled() const { + return _cursorsKilled; + } + +private: + OperationContext* _opCtx; + const SessionKiller::Matcher& _matcher; + std::vector<Status> _failures; + int _cursorsKilled; + Eraser _eraser; +}; + +template <typename Eraser> +auto makeKillCursorsBySessionAdaptor(OperationContext* opCtx, + const SessionKiller::Matcher& matcher, + Eraser&& eraser) { + return KillCursorsBySessionAdaptor<std::decay_t<Eraser>>{ + opCtx, matcher, std::forward<Eraser>(eraser)}; +} + +} // namespace mongo diff --git a/src/mongo/db/session/kill_sessions_local.cpp b/src/mongo/db/session/kill_sessions_local.cpp new file mode 100644 index 00000000000..2665744a4eb --- /dev/null +++ b/src/mongo/db/session/kill_sessions_local.cpp @@ -0,0 +1,249 @@ +/** + * Copyright (C) 2018-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * 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 + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * 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 Server Side 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/session/kill_sessions_local.h" + +#include "mongo/db/client.h" +#include "mongo/db/cursor_manager.h" +#include "mongo/db/operation_context.h" +#include "mongo/db/service_context.h" +#include "mongo/db/session/kill_sessions_common.h" +#include "mongo/db/session/session_catalog.h" +#include "mongo/db/transaction/transaction_participant.h" +#include "mongo/logv2/log.h" + +#define MONGO_LOGV2_DEFAULT_COMPONENT ::mongo::logv2::LogComponent::kCommand + + +namespace mongo { +namespace { + +/** + * Shortcut method shared by the various forms of session kill below. Every session kill operation + * consists of the following stages: + * 1) Select the sessions to kill, based on their lsid or owning user account (achieved through the + * 'matcher') and further refining that list through the 'filterFn'. + * 2) If any of the selected sessions are currently checked out, interrupt the owning operation + * context with 'reason' as the code. + * 3) Finish killing the selected and interrupted sessions through the 'killSessionFn'. + */ +void killSessionsAction( + OperationContext* opCtx, + const SessionKiller::Matcher& matcher, + const std::function<bool(const ObservableSession&)>& filterFn, + const std::function<void(OperationContext*, const SessionToKill&)>& killSessionFn, + ErrorCodes::Error reason = ErrorCodes::Interrupted) { + const auto catalog = SessionCatalog::get(opCtx); + + std::vector<SessionCatalog::KillToken> sessionKillTokens; + catalog->scanSessions(matcher, [&](const ObservableSession& session) { + if (filterFn(session)) + sessionKillTokens.emplace_back(session.kill(reason)); + }); + + for (auto& sessionKillToken : sessionKillTokens) { + auto session = catalog->checkOutSessionForKill(opCtx, std::move(sessionKillToken)); + + // TODO (SERVER-33850): Rename KillAllSessionsByPattern and + // ScopedKillAllSessionsByPatternImpersonator to not refer to session kill + const KillAllSessionsByPattern* pattern = matcher.match(session.getSessionId()); + invariant(pattern); + + ScopedKillAllSessionsByPatternImpersonator impersonator(opCtx, *pattern); + killSessionFn(opCtx, session); + } +} + +} // namespace + +void killSessionsAbortUnpreparedTransactions(OperationContext* opCtx, + const SessionKiller::Matcher& matcher, + ErrorCodes::Error reason) { + killSessionsAction(opCtx, + matcher, + [](const ObservableSession& session) { + auto participant = TransactionParticipant::get(session); + return participant.transactionIsInProgress(); + }, + [](OperationContext* opCtx, const SessionToKill& session) { + auto participant = TransactionParticipant::get(session); + // This is the same test as in the filter, but we must check again now + // that the session is checked out. + if (participant.transactionIsInProgress()) { + participant.abortTransaction(opCtx); + } + }, + reason); +} + +SessionKiller::Result killSessionsLocal(OperationContext* opCtx, + const SessionKiller::Matcher& matcher, + SessionKiller::UniformRandomBitGenerator* urbg) { + killSessionsAbortUnpreparedTransactions(opCtx, matcher); + uassertStatusOK(killSessionsLocalKillOps(opCtx, matcher)); + + auto res = CursorManager::get(opCtx)->killCursorsWithMatchingSessions(opCtx, matcher); + uassertStatusOK(res.first); + + return {std::vector<HostAndPort>{}}; +} + +void killAllExpiredTransactions(OperationContext* opCtx) { + SessionKiller::Matcher matcherAllSessions( + KillAllSessionsByPatternSet{makeKillAllSessionsByPattern(opCtx)}); + killSessionsAction( + opCtx, + matcherAllSessions, + [when = opCtx->getServiceContext()->getPreciseClockSource()->now()]( + const ObservableSession& session) { + return TransactionParticipant::get(session).expiredAsOf(when); + }, + [](OperationContext* opCtx, const SessionToKill& session) { + auto txnParticipant = TransactionParticipant::get(session); + // If the transaction is aborted here, it means it was aborted after + // the filter. The most likely reason for this is that the transaction + // was active and the session kill aborted it. We still want to log + // that as aborted due to transactionLifetimeLimitSessions. + if (txnParticipant.transactionIsInProgress() || txnParticipant.transactionIsAborted()) { + LOGV2( + 20707, + "Aborting transaction with session id {sessionId} and txnNumberAndRetryCounter " + "{txnNumberAndRetryCounter} " + "because it has been running for longer than 'transactionLifetimeLimitSeconds'", + "Aborting transaction because it has been running for longer than " + "'transactionLifetimeLimitSeconds'", + "sessionId"_attr = session.getSessionId().getId(), + "txnNumberAndRetryCounter"_attr = + txnParticipant.getActiveTxnNumberAndRetryCounter()); + if (txnParticipant.transactionIsInProgress()) { + txnParticipant.abortTransaction(opCtx); + } + } + }, + ErrorCodes::TransactionExceededLifetimeLimitSeconds); +} + +void killSessionsLocalShutdownAllTransactions(OperationContext* opCtx) { + SessionKiller::Matcher matcherAllSessions( + KillAllSessionsByPatternSet{makeKillAllSessionsByPattern(opCtx)}); + killSessionsAction(opCtx, + matcherAllSessions, + [](const ObservableSession& session) { + return TransactionParticipant::get(session).transactionIsOpen(); + }, + [](OperationContext* opCtx, const SessionToKill& session) { + TransactionParticipant::get(session).shutdown(opCtx); + }, + ErrorCodes::InterruptedAtShutdown); +} + +void killSessionsAbortAllPreparedTransactions(OperationContext* opCtx) { + SessionKiller::Matcher matcherAllSessions( + KillAllSessionsByPatternSet{makeKillAllSessionsByPattern(opCtx)}); + killSessionsAction(opCtx, + matcherAllSessions, + [](const ObservableSession& session) { + // Filter for sessions that have a prepared transaction. + return TransactionParticipant::get(session).transactionIsPrepared(); + }, + [](OperationContext* opCtx, const SessionToKill& session) { + // We're holding the RSTL, so the transaction shouldn't be otherwise + // affected. + invariant(TransactionParticipant::get(session).transactionIsPrepared()); + // Abort the prepared transaction and invalidate the session it is + // associated with. + TransactionParticipant::get(session).abortTransaction(opCtx); + TransactionParticipant::get(session).invalidate(opCtx); + }); +} + +void yieldLocksForPreparedTransactions(OperationContext* opCtx) { + // Create a new opCtx because we need an empty locker to refresh the locks. + auto newClient = opCtx->getServiceContext()->makeClient("prepared-txns-yield-locks"); + AlternativeClientRegion acr(newClient); + auto newOpCtx = cc().makeOperationContext(); + + // Scan the sessions again to get the list of all sessions with prepared transaction + // to yield their locks. + LOGV2(6015318, "Yielding locks for prepared transactions."); + SessionKiller::Matcher matcherAllSessions( + KillAllSessionsByPatternSet{makeKillAllSessionsByPattern(newOpCtx.get())}); + killSessionsAction( + newOpCtx.get(), + matcherAllSessions, + [](const ObservableSession& session) { + return TransactionParticipant::get(session).transactionIsPrepared(); + }, + [](OperationContext* killerOpCtx, const SessionToKill& session) { + auto txnParticipant = TransactionParticipant::get(session); + // Yield locks for prepared transactions. When scanning and killing + // operations, all prepared transactions are included in the list. Even + // though new sessions may be created after the scan, none of them can + // become prepared during stepdown, since the RSTL has been enqueued, + // preventing any new writes. + if (txnParticipant.transactionIsPrepared()) { + LOGV2_DEBUG(20708, + 3, + "Yielding locks of prepared transaction. SessionId: " + "{sessionId} TxnNumberAndRetryCounter: {txnNumberAndRetryCounter}", + "Yielding locks of prepared transaction", + "sessionId"_attr = session.getSessionId().getId(), + "txnNumberAndRetryCounter"_attr = + txnParticipant.getActiveTxnNumberAndRetryCounter()); + txnParticipant.refreshLocksForPreparedTransaction(killerOpCtx, true); + } + }, + ErrorCodes::InterruptedDueToReplStateChange); +} + +void invalidateSessionsForStepdown(OperationContext* opCtx) { + // It is illegal to invalidate the sessions if the operation has a session checked out. + invariant(!OperationContextSession::get(opCtx)); + + LOGV2(6015319, "Invalidating sessions for stepdown."); + SessionKiller::Matcher matcherAllSessions( + KillAllSessionsByPatternSet{makeKillAllSessionsByPattern(opCtx)}); + killSessionsAction(opCtx, + matcherAllSessions, + [](const ObservableSession& session) { + return !TransactionParticipant::get(session).transactionIsPrepared(); + }, + [](OperationContext* killerOpCtx, const SessionToKill& session) { + auto txnParticipant = TransactionParticipant::get(session); + if (!txnParticipant.transactionIsPrepared()) { + txnParticipant.invalidate(killerOpCtx); + } + }, + ErrorCodes::InterruptedDueToReplStateChange); +} + +} // namespace mongo diff --git a/src/mongo/db/session/kill_sessions_local.h b/src/mongo/db/session/kill_sessions_local.h new file mode 100644 index 00000000000..46b3a72120a --- /dev/null +++ b/src/mongo/db/session/kill_sessions_local.h @@ -0,0 +1,80 @@ +/** + * Copyright (C) 2018-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * 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 + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * 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 Server Side 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 "mongo/db/session/session_killer.h" + +/** + * Mongod local kill session / transaction functionality library. + */ +namespace mongo { + +/** + * Kills all cursors, ops, and transactions on mongod for sessions matching 'matcher'. + */ +SessionKiller::Result killSessionsLocal(OperationContext* opCtx, + const SessionKiller::Matcher& matcher, + SessionKiller::UniformRandomBitGenerator* urbg); + +/** + * Matches only sessions which have unprepared multi-statement transactions, kills the sessions and + * aborts the transactions. + */ +void killSessionsAbortUnpreparedTransactions(OperationContext* opCtx, + const SessionKiller::Matcher& matcher, + ErrorCodes::Error reason = ErrorCodes::Interrupted); + +/** + * Aborts any expired transactions. + */ +void killAllExpiredTransactions(OperationContext* opCtx); + +/** + * Run during shutdown to kill all in-progress transactions, including those in prepare. + */ +void killSessionsLocalShutdownAllTransactions(OperationContext* opCtx); + +/** + * Run during rollback to abort all in-progress prepared transactions. + */ +void killSessionsAbortAllPreparedTransactions(OperationContext* opCtx); + +/** + * Yields locks of prepared transactions. + */ +void yieldLocksForPreparedTransactions(OperationContext* opCtx); + +/** + * Invalidates sessions that do not have prepared transactions, since txnNumbers for transactions + * that were aborted in-memory may be reused on the new primary. + */ +void invalidateSessionsForStepdown(OperationContext* opCtx); + +} // namespace mongo diff --git a/src/mongo/db/session/kill_sessions_util.h b/src/mongo/db/session/kill_sessions_util.h new file mode 100644 index 00000000000..6094073326f --- /dev/null +++ b/src/mongo/db/session/kill_sessions_util.h @@ -0,0 +1,51 @@ +/** + * Copyright (C) 2021-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * 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 + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * 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 Server Side 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 "mongo/base/status.h" +#include "mongo/db/session/logical_session_id_helpers.h" + +namespace mongo { + +namespace kill_sessions { + +/** + * Validation callback to check if a given lsid has an associated parent session. + */ +inline Status validateLsid(const LogicalSessionId& lsid) { + if (isChildSession(lsid)) { + return {ErrorCodes::InvalidOptions, "Cannot kill a child session"}; + } + return Status::OK(); +} + +} // namespace kill_sessions + +} // namespace mongo diff --git a/src/mongo/db/session/logical_session_cache.cpp b/src/mongo/db/session/logical_session_cache.cpp new file mode 100644 index 00000000000..c56e344a833 --- /dev/null +++ b/src/mongo/db/session/logical_session_cache.cpp @@ -0,0 +1,67 @@ +/** + * Copyright (C) 2018-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * 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 + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * 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 Server Side 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/session/logical_session_cache.h" + +#include "mongo/db/operation_context.h" +#include "mongo/db/service_context.h" + +namespace mongo { + +namespace { +const auto getLogicalSessionCache = + ServiceContext::declareDecoration<std::unique_ptr<LogicalSessionCache>>(); + +const auto getLogicalSessionCacheIsRegistered = + ServiceContext::declareDecoration<AtomicWord<bool>>(); +} // namespace + +LogicalSessionCache::~LogicalSessionCache() = default; + +LogicalSessionCache* LogicalSessionCache::get(ServiceContext* service) { + if (getLogicalSessionCacheIsRegistered(service).load()) { + return getLogicalSessionCache(service).get(); + } + return nullptr; +} + +LogicalSessionCache* LogicalSessionCache::get(OperationContext* ctx) { + return get(ctx->getClient()->getServiceContext()); +} + +void LogicalSessionCache::set(ServiceContext* service, + std::unique_ptr<LogicalSessionCache> sessionCache) { + auto& cache = getLogicalSessionCache(service); + cache = std::move(sessionCache); + getLogicalSessionCacheIsRegistered(service).store(true); +} + +} // namespace mongo diff --git a/src/mongo/db/session/logical_session_cache.h b/src/mongo/db/session/logical_session_cache.h new file mode 100644 index 00000000000..051dcac3ad5 --- /dev/null +++ b/src/mongo/db/session/logical_session_cache.h @@ -0,0 +1,150 @@ +/** + * Copyright (C) 2018-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * 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 + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * 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 Server Side 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/base/status.h" +#include "mongo/db/client.h" +#include "mongo/db/operation_context.h" +#include "mongo/db/session/logical_session_cache_gen.h" +#include "mongo/db/session/logical_session_cache_stats_gen.h" +#include "mongo/db/session/logical_session_id_helpers.h" + +namespace mongo { + +/** + * The interface for the logical session cache + */ +class LogicalSessionCache { +public: + /** + * Decorate the ServiceContext with a LogicalSessionCache instance. + */ + static LogicalSessionCache* get(ServiceContext* service); + static LogicalSessionCache* get(OperationContext* opCtx); + static void set(ServiceContext* service, std::unique_ptr<LogicalSessionCache> sessionCache); + + virtual ~LogicalSessionCache() = 0; + + /** + * Invoked on service shutdown time in order to join the cache's refresher and reaper tasks. + */ + virtual void joinOnShutDown() = 0; + + /** + * Inserts a new authoritative session record into the cache. + * + * This method will insert the authoritative record into the sessions collection and should only + * be used when starting new sessions. It should not be used to insert records for existing + * sessions. + */ + virtual Status startSession(OperationContext* opCtx, const LogicalSessionRecord& record) = 0; + + /** + * Vivifies the session in the cache. I.e. creates it if it isn't there, updates last use if it + * is. + */ + virtual Status vivify(OperationContext* opCtx, const LogicalSessionId& lsid) = 0; + + /** + * enqueues LogicalSessionIds for removal during the next _refresh() + */ + virtual void endSessions(const LogicalSessionIdSet& lsids) = 0; + + /** + * Refreshes the cache synchronously. This flushes all pending refreshes and inserts to the + * sessions collection. + */ + virtual Status refreshNow(OperationContext* opCtx) = 0; + + /** + * Reaps transaction records synchronously. + */ + virtual void reapNow(OperationContext* opCtx) = 0; + + /** + * Returns the number of session records currently in the cache. + */ + virtual size_t size() = 0; + + /** + * Ennumerate all LogicalSessionId keys currently in the cache. + */ + virtual std::vector<LogicalSessionId> listIds() const = 0; + + /** + * Ennumerate all LogicalSessionId keys in the cache for the given UserDigests. + */ + virtual std::vector<LogicalSessionId> listIds( + const std::vector<SHA256Block>& userDigest) const = 0; + + /** + * Retrieve a LogicalSessionRecord by LogicalSessionId, if it exists in the cache. + */ + virtual boost::optional<LogicalSessionRecord> peekCached(const LogicalSessionId& id) const = 0; + + /** + * Returns stats about the logical session cache and its recent operations. + */ + virtual LogicalSessionCacheStats getStats() = 0; +}; + +/** + * WARNING: This class should only be used for rare operations because it generates a new logical + * session ID that isn't reaped until the next refresh, which could overwhelm memory if called in a + * loop. + */ +class AlternativeSessionRegion { +public: + AlternativeSessionRegion(OperationContext* opCtx) + : _alternateClient(opCtx->getServiceContext()->makeClient("alternative-session-region")), + _acr(_alternateClient), + _newOpCtx(cc().makeOperationContext()), + _lsid(makeLogicalSessionId(opCtx)) { + _newOpCtx->setLogicalSessionId(_lsid); + } + + ~AlternativeSessionRegion() { + LogicalSessionCache::get(opCtx())->endSessions({_lsid}); + } + + OperationContext* opCtx() { + return &*_newOpCtx; + } + +private: + ServiceContext::UniqueClient _alternateClient; + AlternativeClientRegion _acr; + ServiceContext::UniqueOperationContext _newOpCtx; + LogicalSessionId _lsid; +}; + +} // namespace mongo diff --git a/src/mongo/db/session/logical_session_cache.idl b/src/mongo/db/session/logical_session_cache.idl new file mode 100644 index 00000000000..3294df14378 --- /dev/null +++ b/src/mongo/db/session/logical_session_cache.idl @@ -0,0 +1,75 @@ +# Copyright (C) 2019-present MongoDB, Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the Server Side Public License, version 1, +# as published by MongoDB, Inc. +# +# 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 +# Server Side Public License for more details. +# +# You should have received a copy of the Server Side Public License +# along with this program. If not, see +# <http://www.mongodb.com/licensing/server-side-public-license>. +# +# 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 Server Side 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. +# + +global: + cpp_namespace: "mongo" + +server_parameters: + + logicalSessionRefreshMillis: + description: The interval (in milliseconds) at which the cache refreshes its logical session + records against the main session store. + set_at: startup + cpp_vartype: int + cpp_varname: logicalSessionRefreshMillis + default: 300000 + + maxSessions: + description: The maximum number of sessions that can be cached. + set_at: startup + cpp_vartype: int + cpp_varname: maxSessions + default: 1000000 + + disableLogicalSessionCacheRefresh: + description: Disable the logical session cache refresh (for testing only). + set_at: startup + cpp_vartype: bool + cpp_varname: disableLogicalSessionCacheRefresh + default: false + + TransactionRecordMinimumLifetimeMinutes: + # The minimum lifetime for a transaction record is how long it has to have lived on the server + # before we'll consider it for cleanup. This is effectively the window for how long it is + # permissible for a mongos to hang before we're willing to accept a failure of the retryable + # writes subsystem. + # + # Specifically, we imagine that a client connects to one mongos on a session and performs a + # retryable write after which that mongos hangs. Then the client connects to a new mongos on the + # same session and successfully executes its write. After a day passes, the session will time + # out, cleaning up the retryable write. Then the original mongos wakes up, vivifies the session + # and executes the write (because all records of the session + transaction have been deleted). + # + # So the write is performed twice, which is unavoidable without losing session vivification + # and/or requiring synchronized clocks all the way out to the client. In lieu of that we + # provide a weaker guarantee after the minimum transaction lifetime. + description: The minimum lifetime for a transaction record. + set_at: startup + cpp_vartype: int + cpp_varname: gTransactionRecordMinimumLifetimeMinutes + default: 30 diff --git a/src/mongo/db/session/logical_session_cache_impl.cpp b/src/mongo/db/session/logical_session_cache_impl.cpp new file mode 100644 index 00000000000..3991e614e5d --- /dev/null +++ b/src/mongo/db/session/logical_session_cache_impl.cpp @@ -0,0 +1,460 @@ +/** + * Copyright (C) 2018-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * 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 + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * 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 Server Side 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/session/logical_session_cache_impl.h" + +#include "mongo/db/internal_transactions_feature_flag_gen.h" +#include "mongo/db/operation_context.h" +#include "mongo/db/repl/replication_coordinator.h" +#include "mongo/db/s/operation_sharding_state.h" +#include "mongo/db/service_context.h" +#include "mongo/db/session/logical_session_id.h" +#include "mongo/db/session/logical_session_id_helpers.h" +#include "mongo/logv2/log.h" +#include "mongo/logv2/log_severity_suppressor.h" +#include "mongo/platform/atomic_word.h" +#include "mongo/util/duration.h" +#include "mongo/util/scopeguard.h" + +#define MONGO_LOGV2_DEFAULT_COMPONENT ::mongo::logv2::LogComponent::kControl + + +namespace mongo { + +namespace { + +void clearShardingOperationFailedStatus(OperationContext* opCtx) { + // We do not intend to immediately act upon sharding errors if we receive them during sessions + // collection operations. We will instead attempt the same operations during the next refresh + // cycle. + OperationShardingState::get(opCtx).resetShardingOperationFailedStatus(); +} + +} // namespace + +LogicalSessionCacheImpl::LogicalSessionCacheImpl(std::unique_ptr<ServiceLiaison> service, + std::shared_ptr<SessionsCollection> collection, + ReapSessionsOlderThanFn reapSessionsOlderThanFn) + : _service(std::move(service)), + _sessionsColl(std::move(collection)), + _reapSessionsOlderThanFn(std::move(reapSessionsOlderThanFn)) { + _stats.setLastSessionsCollectionJobTimestamp(_service->now()); + _stats.setLastTransactionReaperJobTimestamp(_service->now()); + + if (!disableLogicalSessionCacheRefresh) { + _service->scheduleJob({"LogicalSessionCacheRefresh", + [this](Client* client) { _periodicRefresh(client); }, + Milliseconds(logicalSessionRefreshMillis)}); + + _service->scheduleJob({"LogicalSessionCacheReap", + [this](Client* client) { _periodicReap(client); }, + Milliseconds(logicalSessionRefreshMillis)}); + } +} + +LogicalSessionCacheImpl::~LogicalSessionCacheImpl() { + joinOnShutDown(); +} + +void LogicalSessionCacheImpl::joinOnShutDown() { + _service->join(); +} + +Status LogicalSessionCacheImpl::startSession(OperationContext* opCtx, + const LogicalSessionRecord& record) { + stdx::lock_guard lg(_mutex); + return _addToCacheIfNotFull(lg, record); +} + +Status LogicalSessionCacheImpl::vivify(OperationContext* opCtx, const LogicalSessionId& lsid) { + auto parentLsid = getParentSessionId(lsid); + + stdx::lock_guard lg(_mutex); + + auto it = _activeSessions.find(parentLsid ? *parentLsid : lsid); + if (it == _activeSessions.end()) + return _addToCacheIfNotFull(lg, makeLogicalSessionRecord(opCtx, lsid, _service->now())); + + auto& cacheEntry = it->second; + cacheEntry.setLastUse(_service->now()); + + return Status::OK(); +} + +Status LogicalSessionCacheImpl::refreshNow(OperationContext* opCtx) { + try { + _refresh(opCtx->getClient()); + } catch (const DBException& ex) { + LOGV2( + 20714, + "Failed to refresh session cache, will try again at the next refresh interval {error}", + "Failed to refresh session cache, will try again at the next refresh interval", + "error"_attr = redact(ex)); + return exceptionToStatus(); + } catch (...) { + return exceptionToStatus(); + } + return Status::OK(); +} + +void LogicalSessionCacheImpl::reapNow(OperationContext* opCtx) { + uassertStatusOK(_reap(opCtx->getClient())); +} + +size_t LogicalSessionCacheImpl::size() { + stdx::lock_guard<Latch> lock(_mutex); + return _activeSessions.size(); +} + +void LogicalSessionCacheImpl::_periodicRefresh(Client* client) { + try { + _refresh(client); + } catch (const DBException& ex) { + LOGV2( + 20710, + "Failed to refresh session cache: {error}, will try again at the next refresh interval", + "Failed to refresh session cache, will try again at the next refresh interval", + "error"_attr = redact(ex)); + } +} + +void LogicalSessionCacheImpl::_periodicReap(Client* client) { + auto res = _reap(client); + if (!res.isOK()) { + LOGV2(20711, + "Failed to reap transaction table: {error}", + "Failed to reap transaction table", + "error"_attr = redact(res)); + } + + return; +} + +Status LogicalSessionCacheImpl::_reap(Client* client) { + boost::optional<ServiceContext::UniqueOperationContext> uniqueCtx; + auto* const opCtx = [&] { + if (client->getOperationContext()) { + return client->getOperationContext(); + } + + uniqueCtx.emplace(client->makeOperationContext()); + return uniqueCtx->get(); + }(); + + const auto replCoord = repl::ReplicationCoordinator::get(opCtx); + if (replCoord && replCoord->isReplEnabled() && replCoord->getMemberState().arbiter()) { + return Status::OK(); + } + + // Take the lock to update some stats. + { + stdx::lock_guard<Latch> lk(_mutex); + + // Clear the last set of stats for our new run. + _stats.setLastTransactionReaperJobDurationMillis(0); + _stats.setLastTransactionReaperJobEntriesCleanedUp(0); + + // Start the new run. + _stats.setLastTransactionReaperJobTimestamp(_service->now()); + _stats.setTransactionReaperJobCount(_stats.getTransactionReaperJobCount() + 1); + } + + int numReaped = 0; + + try { + ON_BLOCK_EXIT([&opCtx] { clearShardingOperationFailedStatus(opCtx); }); + try { + _sessionsColl->checkSessionsCollectionExists(opCtx); + } catch (const DBException& ex) { + LOGV2(20712, + "Sessions collection is not set up: {error}; waiting until next sessions reap " + "interval", + "Sessions collection is not set up; waiting until next sessions reap interval", + "error"_attr = redact(ex)); + return Status::OK(); + } + numReaped = _reapSessionsOlderThanFn(opCtx, + *_sessionsColl, + _service->now() - + Minutes(gTransactionRecordMinimumLifetimeMinutes)); + } catch (const DBException& ex) { + { + stdx::lock_guard<Latch> lk(_mutex); + auto millis = _service->now() - _stats.getLastTransactionReaperJobTimestamp(); + _stats.setLastTransactionReaperJobDurationMillis(millis.count()); + } + + return ex.toStatus(); + } + + { + stdx::lock_guard<Latch> lk(_mutex); + auto millis = _service->now() - _stats.getLastTransactionReaperJobTimestamp(); + _stats.setLastTransactionReaperJobDurationMillis(millis.count()); + _stats.setLastTransactionReaperJobEntriesCleanedUp(numReaped); + } + + return Status::OK(); +} + +void LogicalSessionCacheImpl::_refresh(Client* client) { + // get or make an opCtx + boost::optional<ServiceContext::UniqueOperationContext> uniqueCtx; + auto* const opCtx = [&client, &uniqueCtx] { + if (client->getOperationContext()) { + return client->getOperationContext(); + } + + uniqueCtx.emplace(client->makeOperationContext()); + return uniqueCtx->get(); + }(); + + const auto replCoord = repl::ReplicationCoordinator::get(opCtx); + if (replCoord && replCoord->isReplEnabled() && replCoord->getMemberState().arbiter()) { + stdx::lock_guard<Latch> lk(_mutex); + _activeSessions.clear(); + return; + } + + // Stats for serverStatus: + { + stdx::lock_guard<Latch> lk(_mutex); + + // Clear the refresh-related stats with the beginning of our run. + _stats.setLastSessionsCollectionJobDurationMillis(0); + _stats.setLastSessionsCollectionJobEntriesRefreshed(0); + _stats.setLastSessionsCollectionJobEntriesEnded(0); + _stats.setLastSessionsCollectionJobCursorsClosed(0); + + // Start the new run. + _stats.setLastSessionsCollectionJobTimestamp(_service->now()); + _stats.setSessionsCollectionJobCount(_stats.getSessionsCollectionJobCount() + 1); + } + + // This will finish timing _refresh for our stats no matter when we return. + const ScopeGuard timeRefreshJob([this] { + stdx::lock_guard<Latch> lk(_mutex); + auto millis = _service->now() - _stats.getLastSessionsCollectionJobTimestamp(); + _stats.setLastSessionsCollectionJobDurationMillis(millis.count()); + }); + + ON_BLOCK_EXIT([&opCtx] { clearShardingOperationFailedStatus(opCtx); }); + + _sessionsColl->setupSessionsCollection(opCtx); + + LogicalSessionIdSet staleSessions; + LogicalSessionIdSet explicitlyEndingSessions; + LogicalSessionIdMap<LogicalSessionRecord> activeSessions; + + { + using std::swap; + stdx::lock_guard<Latch> lk(_mutex); + swap(explicitlyEndingSessions, _endingSessions); + swap(activeSessions, _activeSessions); + } + + // Create guards that in the case of a exception replace the ending or active sessions that + // swapped out of LogicalSessionCache, and merges in any records that had been added since we + // swapped them out. + auto backSwap = [this](auto& member, auto& temp) { + stdx::lock_guard<Latch> lk(_mutex); + using std::swap; + swap(member, temp); + for (const auto& it : temp) { + member.emplace(it); + } + }; + ScopeGuard activeSessionsBackSwapper([&] { backSwap(_activeSessions, activeSessions); }); + auto explicitlyEndingBackSwaper = + ScopeGuard([&] { backSwap(_endingSessions, explicitlyEndingSessions); }); + + // remove all explicitlyEndingSessions from activeSessions + for (const auto& lsid : explicitlyEndingSessions) { + activeSessions.erase(lsid); + } + + // Refresh all recently active sessions as well as for sessions attached to running ops + LogicalSessionRecordSet activeSessionRecords; + + auto runningOpSessions = _service->getActiveOpSessions(); + + for (const auto& it : runningOpSessions) { + // if a running op is the cause of an upsert, we won't have a user name for the record + if (explicitlyEndingSessions.count(it) > 0) { + continue; + } + activeSessionRecords.insert(makeLogicalSessionRecord(it, _service->now())); + } + for (const auto& it : activeSessions) { + activeSessionRecords.insert(it.second); + } + + // Refresh the active sessions in the sessions collection. + _sessionsColl->refreshSessions(opCtx, activeSessionRecords); + activeSessionsBackSwapper.dismiss(); + { + stdx::lock_guard<Latch> lk(_mutex); + _stats.setLastSessionsCollectionJobEntriesRefreshed(activeSessionRecords.size()); + } + + // Remove the ending sessions from the sessions collection. + _sessionsColl->removeRecords(opCtx, explicitlyEndingSessions); + explicitlyEndingBackSwaper.dismiss(); + { + stdx::lock_guard<Latch> lk(_mutex); + _stats.setLastSessionsCollectionJobEntriesEnded(explicitlyEndingSessions.size()); + } + + // Find which running, but not recently active sessions, are expired, and add them + // to the list of sessions to kill cursors for + + KillAllSessionsByPatternSet patterns; + + auto openCursorSessions = _service->getOpenCursorSessions(opCtx); + // Exclude sessions added to _activeSessions from the openCursorSession to avoid race between + // killing cursors on the removed sessions and creating sessions. + { + stdx::lock_guard<Latch> lk(_mutex); + + for (const auto& it : _activeSessions) { + auto newSessionIt = openCursorSessions.find(it.first); + if (newSessionIt != openCursorSessions.end()) { + openCursorSessions.erase(newSessionIt); + } + } + } + + // think about pruning ending and active out of openCursorSessions + try { + auto removedSessions = _sessionsColl->findRemovedSessions(opCtx, openCursorSessions); + + for (const auto& lsid : removedSessions) { + patterns.emplace(makeKillAllSessionsByPattern(opCtx, lsid)); + } + } catch (...) { + // Ignore errors. + } + + // Add all of the explicitly ended sessions to the list of sessions to kill cursors for. + for (const auto& lsid : explicitlyEndingSessions) { + patterns.emplace(makeKillAllSessionsByPattern(opCtx, lsid)); + } + + SessionKiller::Matcher matcher(std::move(patterns)); + auto killRes = _service->killCursorsWithMatchingSessions(opCtx, std::move(matcher)); + { + stdx::lock_guard<Latch> lk(_mutex); + _stats.setLastSessionsCollectionJobCursorsClosed(killRes.second); + } +} + +void LogicalSessionCacheImpl::endSessions(const LogicalSessionIdSet& sessions) { + for (const auto& lsid : sessions) { + uassert(ErrorCodes::InvalidOptions, + str::stream() << "Cannot specify a child session id " << lsid, + isParentSessionId(lsid)); + } + stdx::lock_guard<Latch> lk(_mutex); + _endingSessions.insert(begin(sessions), end(sessions)); +} + +LogicalSessionCacheStats LogicalSessionCacheImpl::getStats() { + stdx::lock_guard<Latch> lk(_mutex); + _stats.setActiveSessionsCount(_activeSessions.size()); + return _stats; +} + +Status LogicalSessionCacheImpl::_addToCacheIfNotFull(WithLock, LogicalSessionRecord record) { + if (_activeSessions.size() >= size_t(maxSessions)) { + Status status = {ErrorCodes::TooManyLogicalSessions, + str::stream() + << "Unable to add session ID " << record.getId() + << " into the cache because the number of active sessions is too " + "high"}; + // Returns Info() unless it was called in the past second. + // In that case it will return the quieter Debug(2) */ + static auto& bumpedSeverity = *new logv2::SeveritySuppressor{ + Seconds{1}, logv2::LogSeverity::Info(), logv2::LogSeverity::Debug(2)}; + LOGV2_DEBUG(20715, + bumpedSeverity().toInt(), + "Unable to add session {sessionId} into the cache, too many active sessions: " + "{sessionCount}, maximum: {maxSessions}", + "Unable to add session into the cache, too many active sessions", + "sessionId"_attr = record.getId(), + "sessionCount"_attr = _activeSessions.size(), + "maxSessions"_attr = maxSessions); + return status; + } + + _activeSessions.insert(std::make_pair(record.getId(), std::move(record))); + + return Status::OK(); +} + +std::vector<LogicalSessionId> LogicalSessionCacheImpl::listIds() const { + stdx::lock_guard<Latch> lk(_mutex); + std::vector<LogicalSessionId> ret; + ret.reserve(_activeSessions.size()); + for (const auto& id : _activeSessions) { + ret.push_back(id.first); + } + return ret; +} + +std::vector<LogicalSessionId> LogicalSessionCacheImpl::listIds( + const std::vector<SHA256Block>& userDigests) const { + stdx::lock_guard<Latch> lk(_mutex); + std::vector<LogicalSessionId> ret; + for (const auto& it : _activeSessions) { + if (std::find(userDigests.cbegin(), userDigests.cend(), it.first.getUid()) != + userDigests.cend()) { + ret.push_back(it.first); + } + } + return ret; +} + +boost::optional<LogicalSessionRecord> LogicalSessionCacheImpl::peekCached( + const LogicalSessionId& lsid) const { + uassert(ErrorCodes::InvalidOptions, + str::stream() << "Cannot specify a child session id " << lsid, + isParentSessionId(lsid)); + + stdx::lock_guard<Latch> lk(_mutex); + const auto it = _activeSessions.find(lsid); + if (it == _activeSessions.end()) { + return boost::none; + } + return it->second; +} + +} // namespace mongo diff --git a/src/mongo/db/session/logical_session_cache_impl.h b/src/mongo/db/session/logical_session_cache_impl.h new file mode 100644 index 00000000000..c23b6435882 --- /dev/null +++ b/src/mongo/db/session/logical_session_cache_impl.h @@ -0,0 +1,122 @@ +/** + * Copyright (C) 2018-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * 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 + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * 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 Server Side 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 "mongo/db/service_liaison.h" +#include "mongo/db/session/logical_session_cache.h" +#include "mongo/db/session/sessions_collection.h" +#include "mongo/util/concurrency/with_lock.h" +#include "mongo/util/functional.h" +#include "mongo/util/hierarchical_acquisition.h" + +namespace mongo { + +/** + * A thread-safe cache structure for logical session records. + * + * The cache takes ownership of the passed-in ServiceLiaison and SessionsCollection helper types. + * + * Uses the following service-wide parameters: + * - A timeout value to use for sessions in the cache, in minutes. Defaults to 30 minutes. + * --setParameter localLogicalSessionTimeoutMinutes=X + * + * - The interval over which the cache will refresh session records. By default, this is set to + * every 5 minutes (300,000). If the caller is setting the sessionTimeout by hand, it is + * suggested that they consider also setting the refresh interval accordingly. + * --setParameter logicalSessionRefreshMillis=X. + */ +class LogicalSessionCacheImpl final : public LogicalSessionCache { +public: + using ReapSessionsOlderThanFn = + unique_function<int(OperationContext*, SessionsCollection&, Date_t)>; + + LogicalSessionCacheImpl(std::unique_ptr<ServiceLiaison> service, + std::shared_ptr<SessionsCollection> collection, + ReapSessionsOlderThanFn reapSessionsOlderThanFn); + + LogicalSessionCacheImpl(const LogicalSessionCacheImpl&) = delete; + LogicalSessionCacheImpl& operator=(const LogicalSessionCacheImpl&) = delete; + + ~LogicalSessionCacheImpl(); + + void joinOnShutDown() override; + + Status startSession(OperationContext* opCtx, const LogicalSessionRecord& record) override; + + Status vivify(OperationContext* opCtx, const LogicalSessionId& lsid) override; + + Status refreshNow(OperationContext* opCtx) override; + + void reapNow(OperationContext* opCtx) override; + + size_t size() override; + + std::vector<LogicalSessionId> listIds() const override; + + std::vector<LogicalSessionId> listIds( + const std::vector<SHA256Block>& userDigest) const override; + + boost::optional<LogicalSessionRecord> peekCached(const LogicalSessionId& id) const override; + + void endSessions(const LogicalSessionIdSet& sessions) override; + + LogicalSessionCacheStats getStats() override; + +private: + void _periodicRefresh(Client* client); + void _refresh(Client* client); + + void _periodicReap(Client* client); + Status _reap(Client* client); + + /** + * Returns true if a record has passed its given expiration. + */ + bool _isDead(const LogicalSessionRecord& record, Date_t now) const; + + Status _addToCacheIfNotFull(WithLock, LogicalSessionRecord record); + + const std::unique_ptr<ServiceLiaison> _service; + const std::shared_ptr<SessionsCollection> _sessionsColl; + const ReapSessionsOlderThanFn _reapSessionsOlderThanFn; + + mutable Mutex _mutex = + MONGO_MAKE_LATCH(HierarchicalAcquisitionLevel(0), "LogicalSessionCacheImpl::_mutex"); + + LogicalSessionIdMap<LogicalSessionRecord> _activeSessions; + + LogicalSessionIdSet _endingSessions; + + Date_t _lastRefreshTime; + + LogicalSessionCacheStats _stats; +}; + +} // namespace mongo diff --git a/src/mongo/db/session/logical_session_cache_noop.h b/src/mongo/db/session/logical_session_cache_noop.h new file mode 100644 index 00000000000..7ba8f593d3d --- /dev/null +++ b/src/mongo/db/session/logical_session_cache_noop.h @@ -0,0 +1,81 @@ +/** + * Copyright (C) 2018-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * 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 + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * 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 Server Side 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 "mongo/db/session/logical_session_cache.h" + +namespace mongo { + +/** + * A noop logical session cache for use in tests + */ +class LogicalSessionCacheNoop : public LogicalSessionCache { +public: + void joinOnShutDown() override {} + + Status startSession(OperationContext* opCtx, const LogicalSessionRecord& record) override { + return Status::OK(); + } + + Status vivify(OperationContext* opCtx, const LogicalSessionId& lsid) override { + return Status::OK(); + } + + Status refreshNow(OperationContext* opCtx) override { + return Status::OK(); + } + + void reapNow(OperationContext* opCtx) override {} + + size_t size() override { + return 0; + } + + std::vector<LogicalSessionId> listIds() const override { + return {}; + } + + std::vector<LogicalSessionId> listIds( + const std::vector<SHA256Block>& userDigest) const override { + return {}; + } + + boost::optional<LogicalSessionRecord> peekCached(const LogicalSessionId& id) const override { + return boost::none; + } + + LogicalSessionCacheStats getStats() override { + return {}; + }; + + void endSessions(const LogicalSessionIdSet& lsids) override {} +}; + +} // namespace mongo diff --git a/src/mongo/db/session/logical_session_cache_stats.idl b/src/mongo/db/session/logical_session_cache_stats.idl new file mode 100644 index 00000000000..b0adff82caa --- /dev/null +++ b/src/mongo/db/session/logical_session_cache_stats.idl @@ -0,0 +1,76 @@ +# Copyright (C) 2018-present MongoDB, Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the Server Side Public License, version 1, +# as published by MongoDB, Inc. +# +# 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 +# Server Side Public License for more details. +# +# You should have received a copy of the Server Side Public License +# along with this program. If not, see +# <http://www.mongodb.com/licensing/server-side-public-license>. +# +# 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 Server Side 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. +# + +# This IDL file describes the BSON format for a LogicalSessionId, and +# handles the serialization to and deserialization from its BSON representation +# for that class. + +global: + cpp_namespace: "mongo" + +imports: + - "mongo/idl/basic_types.idl" + +structs: + + LogicalSessionCacheStats: + description: "A struct representing the section of the server status + command with information about the logical session cache" + strict: true + fields: + activeSessionsCount: + type: int + default: 0 + sessionsCollectionJobCount: + type: int + default: 0 + lastSessionsCollectionJobDurationMillis: + type: int + default: 0 + lastSessionsCollectionJobTimestamp: + type: date + lastSessionsCollectionJobEntriesRefreshed: + type: int + default: 0 + lastSessionsCollectionJobEntriesEnded: + type: int + default: 0 + lastSessionsCollectionJobCursorsClosed: + type: int + default: 0 + transactionReaperJobCount: + type: int + default: 0 + lastTransactionReaperJobDurationMillis: + type: int + default: 0 + lastTransactionReaperJobTimestamp: + type: date + lastTransactionReaperJobEntriesCleanedUp: + type: int + default: 0 diff --git a/src/mongo/db/session/logical_session_cache_test.cpp b/src/mongo/db/session/logical_session_cache_test.cpp new file mode 100644 index 00000000000..8d9220ebfb8 --- /dev/null +++ b/src/mongo/db/session/logical_session_cache_test.cpp @@ -0,0 +1,448 @@ +/** + * Copyright (C) 2018-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * 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 + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * 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 Server Side 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/session/logical_session_cache_impl.h" + +#include <memory> + +#include "mongo/bson/oid.h" +#include "mongo/db/auth/authorization_manager.h" +#include "mongo/db/auth/authorization_session_for_test.h" +#include "mongo/db/auth/authz_manager_external_state_mock.h" +#include "mongo/db/auth/authz_session_external_state_mock.h" +#include "mongo/db/auth/user_name.h" +#include "mongo/db/operation_context.h" +#include "mongo/db/service_context.h" +#include "mongo/db/service_context_test_fixture.h" +#include "mongo/db/service_liaison_mock.h" +#include "mongo/db/session/logical_session_cache.h" +#include "mongo/db/session/logical_session_id.h" +#include "mongo/db/session/logical_session_id_helpers.h" +#include "mongo/db/session/sessions_collection_mock.h" +#include "mongo/idl/server_parameter_test_util.h" +#include "mongo/stdx/future.h" +#include "mongo/unittest/ensure_fcv.h" +#include "mongo/unittest/unittest.h" + +namespace mongo { +namespace { + +const Milliseconds kSessionTimeout = duration_cast<Milliseconds>(kLogicalSessionDefaultTimeout); +const Milliseconds kForceRefresh{kLogicalSessionRefreshMillisDefault}; + +using SessionList = std::list<LogicalSessionId>; +using unittest::EnsureFCV; + +/** + * Test fixture that sets up a session cache attached to a mock service liaison + * and mock sessions collection implementation. + */ +class LogicalSessionCacheTest : public ServiceContextTest { +public: + LogicalSessionCacheTest() + : _service(std::make_shared<MockServiceLiaisonImpl>()), + _sessions(std::make_shared<MockSessionsCollectionImpl>()) { + + AuthorizationManager::set(getServiceContext(), + AuthorizationManager::create(getServiceContext())); + + // Re-initialize the client after setting the AuthorizationManager to get an + // AuthorizationSession. + Client::releaseCurrent(); + Client::initThread(getThreadName()); + _opCtx = makeOperationContext(); + + auto mockService = std::make_unique<MockServiceLiaison>(_service); + auto mockSessions = std::make_unique<MockSessionsCollection>(_sessions); + _cache = std::make_unique<LogicalSessionCacheImpl>( + std::move(mockService), + std::move(mockSessions), + [](OperationContext*, SessionsCollection&, Date_t) { + return 0; /* No op*/ + }); + } + + void waitUntilRefreshScheduled() { + while (service()->jobs() < 2) { + sleepmillis(10); + } + } + + std::unique_ptr<LogicalSessionCache>& cache() { + return _cache; + } + + std::shared_ptr<MockServiceLiaisonImpl> service() { + return _service; + } + + std::shared_ptr<MockSessionsCollectionImpl> sessions() { + return _sessions; + } + + OperationContext* opCtx() { + return _opCtx.get(); + } + +private: + ServiceContext::UniqueOperationContext _opCtx; + + std::shared_ptr<MockServiceLiaisonImpl> _service; + std::shared_ptr<MockSessionsCollectionImpl> _sessions; + + std::unique_ptr<LogicalSessionCache> _cache; +}; + +TEST_F(LogicalSessionCacheTest, ParentAndChildSessionsHaveEqualLogicalSessionRecord) { + auto parentLsid = makeLogicalSessionIdForTest(); + auto lastUse = Date_t::now(); + auto parentSessionRecord = makeLogicalSessionRecord(parentLsid, lastUse); + + auto childSessionRecord0 = makeLogicalSessionRecord( + makeLogicalSessionIdWithTxnNumberAndUUIDForTest(parentLsid), lastUse); + ASSERT_BSONOBJ_EQ(parentSessionRecord.toBSON(), childSessionRecord0.toBSON()); + + auto childSessionRecord1 = + makeLogicalSessionRecord(makeLogicalSessionIdWithTxnUUIDForTest(parentLsid), lastUse); + ASSERT_BSONOBJ_EQ(parentSessionRecord.toBSON(), childSessionRecord1.toBSON()); +} + +// Test that promoting from the cache updates the lastUse date of records +TEST_F(LogicalSessionCacheTest, VivifyUpdatesLastUse) { + auto runTest = [&](const LogicalSessionId& lsid) { + auto start = service()->now(); + + // Insert the record into the sessions collection with 'start' + ASSERT_OK(cache()->startSession(opCtx(), makeLogicalSessionRecord(lsid, start))); + + // Fast forward time and promote + service()->fastForward(Milliseconds(500)); + ASSERT_OK(cache()->vivify(opCtx(), lsid)); + + // Now that we promoted, lifetime of session should be extended + service()->fastForward(kSessionTimeout - Milliseconds(500)); + ASSERT_OK(cache()->vivify(opCtx(), lsid)); + + // We promoted again, so lifetime extended again + service()->fastForward(kSessionTimeout - Milliseconds(500)); + ASSERT_OK(cache()->vivify(opCtx(), lsid)); + + // Fast forward and promote + service()->fastForward(kSessionTimeout - Milliseconds(10)); + ASSERT_OK(cache()->vivify(opCtx(), lsid)); + + // Lifetime extended again + service()->fastForward(Milliseconds(11)); + ASSERT_OK(cache()->vivify(opCtx(), lsid)); + }; + + runTest(makeLogicalSessionIdForTest()); + runTest(makeLogicalSessionIdWithTxnNumberAndUUIDForTest()); + runTest(makeLogicalSessionIdWithTxnUUIDForTest()); +} + +TEST_F(LogicalSessionCacheTest, VivifyUpdatesLastUseOfParentSession) { + auto runTest = [&](const LogicalSessionId& parentLsid, const LogicalSessionId& childLsid) { + ASSERT_OK( + cache()->startSession(opCtx(), makeLogicalSessionRecord(parentLsid, service()->now()))); + service()->fastForward(Minutes(1)); + ASSERT_OK(cache()->vivify(opCtx(), childLsid)); + ASSERT_OK(cache()->refreshNow(opCtx())); + + auto records = sessions()->sessions(); + ASSERT_EQ(1, records.size()); + ASSERT_EQ(service()->now(), records.begin()->second.getLastUse()); + sessions()->clearSessions(); + }; + + auto parentLsid = makeLogicalSessionIdForTest(); + runTest(parentLsid, makeLogicalSessionIdWithTxnNumberAndUUIDForTest(parentLsid)); + runTest(parentLsid, makeLogicalSessionIdWithTxnUUIDForTest(parentLsid)); + runTest(makeLogicalSessionIdWithTxnNumberAndUUIDForTest(parentLsid), + makeLogicalSessionIdWithTxnUUIDForTest(parentLsid)); +} + +// Test the startSession method +TEST_F(LogicalSessionCacheTest, StartSession) { + auto runTest = [&](const LogicalSessionId& lsid0, const LogicalSessionId& lsid1) { + auto parentLsid0 = getParentSessionId(lsid0); + auto record = makeLogicalSessionRecord(lsid0, service()->now()); + + // Test starting a new session + ASSERT_OK(cache()->startSession(opCtx(), record)); + + // Record will not be in the collection yet; refresh must happen first. + ASSERT(!sessions()->has(lsid0)); + + // Do refresh, cached records should get flushed to collection. + ASSERT(cache()->refreshNow(opCtx()).isOK()); + if (parentLsid0) { + ASSERT(!sessions()->has(lsid0)); + ASSERT(sessions()->has(*parentLsid0)); + } else { + ASSERT(sessions()->has(lsid0)); + } + + // Try to start the same session again, should succeed. + ASSERT_OK(cache()->startSession(opCtx(), record)); + + // Try to start a session that is already in the sessions collection but + // is not in our local cache, should succeed. + auto record1 = makeLogicalSessionRecord(lsid1, service()->now()); + sessions()->add(record1); + ASSERT_OK(cache()->startSession(opCtx(), record1)); + + // Try to start a session that has expired from our cache, and is no + // longer in the sessions collection, should succeed + service()->fastForward(Milliseconds(kSessionTimeout.count() + 5)); + sessions()->remove(parentLsid0 ? *parentLsid0 : lsid0); + if (parentLsid0) { + ASSERT(!sessions()->has(lsid0)); + ASSERT(!sessions()->has(*parentLsid0)); + } else { + ASSERT(!sessions()->has(lsid0)); + } + ASSERT_OK(cache()->startSession(opCtx(), record)); + }; + + runTest(makeLogicalSessionIdForTest(), makeLogicalSessionIdForTest()); + runTest(makeLogicalSessionIdWithTxnNumberAndUUIDForTest(), + makeLogicalSessionIdWithTxnNumberAndUUIDForTest()); + runTest(makeLogicalSessionIdWithTxnUUIDForTest(), makeLogicalSessionIdWithTxnUUIDForTest()); +} + +// Test the endSessions method. +TEST_F(LogicalSessionCacheTest, EndSessions) { + const auto lsids = []() -> std::vector<LogicalSessionId> { + auto lsid0 = makeLogicalSessionIdForTest(); + auto lsid1 = makeLogicalSessionIdForTest(); + auto lsid2 = makeLogicalSessionIdWithTxnNumberAndUUIDForTest(lsid1); + auto lsid3 = makeLogicalSessionIdWithTxnUUIDForTest(lsid1); + return {lsid0, lsid1, lsid2, lsid3}; + }(); + + for (const auto& lsid : lsids) { + ASSERT_OK(cache()->startSession(opCtx(), makeLogicalSessionRecord(lsid, service()->now()))); + } + ASSERT_EQ(2UL, cache()->size()); + + // Verify that it is invalid to pass an lsid with a parent lsid into endSessions. + ASSERT_THROWS_CODE(cache()->endSessions({lsids[2]}), DBException, ErrorCodes::InvalidOptions); + ASSERT_THROWS_CODE(cache()->endSessions({lsids[3]}), DBException, ErrorCodes::InvalidOptions); + + cache()->endSessions({lsids[0], lsids[1]}); +} + +// Test the peekCached method. +TEST_F(LogicalSessionCacheTest, PeekCached) { + auto lsid0 = makeLogicalSessionIdForTest(); + auto record0 = makeLogicalSessionRecord(lsid0, service()->now()); + ASSERT_OK(cache()->startSession(opCtx(), record0)); + ASSERT_BSONOBJ_EQ(record0.toBSON(), cache()->peekCached(lsid0)->toBSON()); + + // Verify that it is invalid to pass an lsid with a parent lsid into peekCached. + auto lsid1 = makeLogicalSessionIdWithTxnNumberAndUUIDForTest(lsid0); + ASSERT_THROWS_CODE(cache()->peekCached(lsid1), DBException, ErrorCodes::InvalidOptions); + auto lsid2 = makeLogicalSessionIdWithTxnUUIDForTest(lsid0); + ASSERT_THROWS_CODE(cache()->peekCached(lsid2), DBException, ErrorCodes::InvalidOptions); +} + +// Test that session cache properly expires lsids after 30 minutes of no use +TEST_F(LogicalSessionCacheTest, BasicSessionExpiration) { + // Insert a lsid + auto record = makeLogicalSessionRecordForTest(); + ASSERT_OK(cache()->startSession(opCtx(), record)); + ASSERT_EQ(1UL, cache()->size()); + + // Force it to expire + service()->fastForward(Milliseconds(kSessionTimeout.count() + 5)); + + // Check that it is no longer in the cache + ASSERT_OK(cache()->refreshNow(opCtx())); + ASSERT_EQ(0UL, cache()->size()); +} + +// Test large sets of cache-only session lsids +TEST_F(LogicalSessionCacheTest, ManySignedLsidsInCacheRefresh) { + int count = 10000; + for (int i = 0; i < count; i++) { + auto record = makeLogicalSessionRecordForTest(); + ASSERT_OK(cache()->startSession(opCtx(), record)); + } + + // Check that all signedLsids refresh + sessions()->setRefreshHook([&count](const LogicalSessionRecordSet& sessions) { + ASSERT_EQ(sessions.size(), size_t(count)); + return Status::OK(); + }); + + // Force a refresh + service()->fastForward(kForceRefresh); + ASSERT_OK(cache()->refreshNow(opCtx())); +} + +// +TEST_F(LogicalSessionCacheTest, RefreshMatrixSessionState) { + const std::vector<std::vector<std::string>> stateNames = { + {"active", "inactive"}, + {"running", "not running"}, + {"expired", "unexpired"}, + {"ended", "not ended"}, + {"cursor", "no cursor"}, + }; + struct { + // results that we test for after the _refresh + bool inCollection; + bool killed; + } testCases[] = { + // 0, active, running, expired, ended, cursor + {false, true}, + // 1, inactive, running, expired, ended, cursor + {false, true}, + // 2, active, not running, expired, ended, cursor + {false, true}, + // 3, inactive, not running, expired, ended, cursor + {false, true}, + // 4, active, running, unexpired, ended, cursor + {false, true}, + // 5, inactive, running, unexpired, ended, cursor + {false, true}, + // 6, active, not running, unexpired, ended, cursor + {false, true}, + // 7, inactive, not running, unexpired, ended, cursor + {false, true}, + // 8, active, running, expired, not ended, cursor + {true, false}, + // 9, inactive, running, expired, not ended, cursor + {false, true}, + // 10, active, not running, expired, not ended, cursor + {true, false}, + // 11, inactive, not running, expired, not ended, cursor + {false, true}, + // 12, active, running, unexpired, not ended, cursor + {true, false}, + // 13, inactive, running, unexpired, not ended, cursor + {true, false}, + // 14, active, not running, unexpired, not ended, cursor + {true, false}, + // 15, inactive, not running, unexpired, not ended, cursor + {true, false}, + // 16, active, running, expired, ended, no cursor + {false, true}, + // 17, inactive, running, expired, ended, no cursor + {false, true}, + // 18, active, not running, expired, ended, no cursor + {false, true}, + // 19, inactive, not running, expired, ended, no cursor + {false, true}, + // 20, active, running, unexpired, ended, no cursor + {false, true}, + // 21, inactive, running, unexpired, ended, no cursor + {false, true}, + // 22, active, not running, unexpired, ended, no cursor + {false, true}, + // 23, inactive, not running, unexpired, ended, no cursor + {false, true}, + // 24, active, running, expired, not ended, no cursor + {true, false}, + // 25, inactive, running, expired, not ended, no cursor + {false, true}, + // 26, active, not running, expired, not ended, no cursor + {true, false}, + // 27, inactive, not running, expired, not ended, no cursor + {false, false}, + // 28, active, running, unexpired, not ended, no cursor + {true, false}, + // 29, inactive, running, unexpired, not ended, no cursor + {true, false}, + // 30, active, not running, unexpired, not ended, no cursor + {true, false}, + // 31, inactive, not running, unexpired, not ended, no cursor + {true, false}, + }; + + std::vector<LogicalSessionId> ids; + for (int i = 0; i < 32; i++) { + + bool active = !(i & 1); + bool running = !(i & 2); + bool expired = !(i & 4); + bool ended = !(i & 8); + bool cursor = !(i & 16); + + auto lsid = makeLogicalSessionIdForTest(); + ids.push_back(lsid); + auto lsRecord = makeLogicalSessionRecord(lsid, service()->now() + Milliseconds(500)); + + if (running) { + service()->add(lsid); + } + if (active) { + ASSERT_OK(cache()->startSession(opCtx(), lsRecord)); + } + if (!expired) { + sessions()->add(lsRecord); + } + if (ended) { + LogicalSessionIdSet lsidSet; + lsidSet.emplace(lsid); + cache()->endSessions(lsidSet); + } + if (cursor) { + service()->addCursorSession(lsid); + } + } + + // Force a refresh + service()->fastForward(kForceRefresh); + ASSERT_OK(cache()->refreshNow(opCtx())); + + for (int i = 0; i < 32; i++) { + std::stringstream failText; + failText << "case " << i << " : "; + for (int j = 0; j < 4; j++) { + failText << stateNames[j][i >> j & 1] << " "; + } + failText << " session case failed: "; + + ASSERT(sessions()->has(ids[i]) == testCases[i].inCollection) + << failText.str() + << (testCases[i].inCollection ? "session wasn't in collection" + : "session was in collection"); + ASSERT((service()->matchKilled(ids[i]) != nullptr) == testCases[i].killed) + << failText.str() + << (testCases[i].killed ? "session wasn't killed" : "session was killed"); + } +} + + +} // namespace +} // namespace mongo diff --git a/src/mongo/db/session/logical_session_id.cpp b/src/mongo/db/session/logical_session_id.cpp new file mode 100644 index 00000000000..d5d4d76af2c --- /dev/null +++ b/src/mongo/db/session/logical_session_id.cpp @@ -0,0 +1,70 @@ +/** + * Copyright (C) 2018-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * 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 + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * 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 Server Side 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/session/logical_session_id.h" + +namespace mongo { + +LogicalSessionId makeLogicalSessionIdForTest() { + LogicalSessionId lsid; + + lsid.setId(UUID::gen()); + lsid.setUid(SHA256Block::computeHash({})); + + return lsid; +} + +LogicalSessionId makeLogicalSessionIdWithTxnNumberAndUUIDForTest( + boost::optional<LogicalSessionId> parentLsid, boost::optional<TxnNumber> parentTxnNumber) { + auto lsid = parentLsid ? LogicalSessionId(parentLsid->getId(), parentLsid->getUid()) + : makeLogicalSessionIdForTest(); + lsid.setTxnUUID(UUID::gen()); + lsid.setTxnNumber(parentTxnNumber ? *parentTxnNumber : 0); + return lsid; +} + +LogicalSessionId makeLogicalSessionIdWithTxnUUIDForTest( + boost::optional<LogicalSessionId> parentLsid) { + auto lsid = parentLsid ? LogicalSessionId(parentLsid->getId(), parentLsid->getUid()) + : makeLogicalSessionIdForTest(); + lsid.setTxnUUID(UUID::gen()); + return lsid; +} + +LogicalSessionRecord makeLogicalSessionRecordForTest() { + LogicalSessionRecord record{}; + + record.setId(makeLogicalSessionIdForTest()); + + return record; +} + +} // namespace mongo diff --git a/src/mongo/db/session/logical_session_id.h b/src/mongo/db/session/logical_session_id.h new file mode 100644 index 00000000000..bb7bb148669 --- /dev/null +++ b/src/mongo/db/session/logical_session_id.h @@ -0,0 +1,198 @@ +/** + * Copyright (C) 2018-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * 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 + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * 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 Server Side 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/functional/hash.hpp> +#include <string> + +#include "mongo/base/status_with.h" +#include "mongo/db/session/logical_session_id_gen.h" +#include "mongo/stdx/unordered_set.h" +#include "mongo/util/uuid.h" + +namespace mongo { + +using TxnNumber = std::int64_t; +using StmtId = std::int32_t; +using TxnRetryCounter = std::int32_t; + +// Default value for unassigned statementId. +const StmtId kUninitializedStmtId = -1; + +// Used as a substitute statementId for oplog entries that were truncated and lost. +const StmtId kIncompleteHistoryStmtId = -2; + +const TxnNumber kUninitializedTxnNumber = -1; +const TxnRetryCounter kUninitializedTxnRetryCounter = -1; + +class BSONObjBuilder; +class OperationContext; + +// The constant kLocalLogicalSessionTimeoutMinutesDefault comes from the generated +// header logical_session_id_gen.h. +constexpr Minutes kLogicalSessionDefaultTimeout = + Minutes(kLocalLogicalSessionTimeoutMinutesDefault); + +inline bool operator==(const LogicalSessionId& lhs, const LogicalSessionId& rhs) { + return (lhs.getId() == rhs.getId()) && (lhs.getTxnNumber() == rhs.getTxnNumber()) && + (lhs.getTxnUUID() == rhs.getTxnUUID()) && (lhs.getUid() == rhs.getUid()); +} + +inline bool operator!=(const LogicalSessionId& lhs, const LogicalSessionId& rhs) { + return !(lhs == rhs); +} + +inline bool operator==(const LogicalSessionRecord& lhs, const LogicalSessionRecord& rhs) { + return lhs.getId() == rhs.getId(); +} + +inline bool operator!=(const LogicalSessionRecord& lhs, const LogicalSessionRecord& rhs) { + return !(lhs == rhs); +} + +LogicalSessionId makeLogicalSessionIdForTest(); + +LogicalSessionId makeLogicalSessionIdWithTxnNumberAndUUIDForTest( + boost::optional<LogicalSessionId> parentLsid = boost::none, + boost::optional<TxnNumber> parentTxnNumber = boost::none); + +LogicalSessionId makeLogicalSessionIdWithTxnUUIDForTest( + boost::optional<LogicalSessionId> parentLsid = boost::none); + +LogicalSessionRecord makeLogicalSessionRecordForTest(); + +struct LogicalSessionIdHash { + std::size_t operator()(const LogicalSessionId& lsid) const { + // Due to internal sessions having the same _id, we want to hash by its txnUUID and + // txnNumber to discourage hash key collision. + if (auto txnUUID = lsid.getTxnUUID()) { + auto hash = _hasher(*txnUUID); + if (lsid.getTxnNumber()) { + boost::hash_combine(hash, *lsid.getTxnNumber()); + } + return hash; + } + return _hasher(lsid.getId()); + } + +private: + UUID::Hash _hasher; +}; + +struct LogicalSessionRecordHash { + std::size_t operator()(const LogicalSessionRecord& lsid) const { + return LogicalSessionIdHash{}(lsid.getId()); + } + +private: + UUID::Hash _hasher; +}; + + +inline std::ostream& operator<<(std::ostream& s, const LogicalSessionId& lsid) { + return (s << lsid.getId() << " - " << lsid.getUid() << " - " + << (lsid.getTxnNumber() ? std::to_string(*lsid.getTxnNumber()) : "") << " - " + << (lsid.getTxnUUID() ? lsid.getTxnUUID()->toString() : "")); +} + +inline StringBuilder& operator<<(StringBuilder& s, const LogicalSessionId& lsid) { + return (s << lsid.getId().toString() << " - " << lsid.getUid().toString() << " - " + << (lsid.getTxnNumber() ? std::to_string(*lsid.getTxnNumber()) : "") << " - " + << (lsid.getTxnUUID() ? lsid.getTxnUUID()->toString() : "")); +} + +inline std::ostream& operator<<(std::ostream& s, const LogicalSessionFromClient& lsid) { + return (s << lsid.getId() << " - " << (lsid.getUid() ? lsid.getUid()->toString() : "") << " - " + << (lsid.getTxnNumber() ? std::to_string(*lsid.getTxnNumber()) : "") << " - " + << (lsid.getTxnUUID() ? lsid.getTxnUUID()->toString() : "")); +} + +inline StringBuilder& operator<<(StringBuilder& s, const LogicalSessionFromClient& lsid) { + return (s << lsid.getId() << " - " << (lsid.getUid() ? lsid.getUid()->toString() : "") << " - " + << (lsid.getTxnNumber() ? std::to_string(*lsid.getTxnNumber()) : "") << " - " + << (lsid.getTxnUUID() ? lsid.getTxnUUID()->toString() : "")); +} + +/** + * An alias for sets of session ids. + */ +using LogicalSessionIdSet = stdx::unordered_set<LogicalSessionId, LogicalSessionIdHash>; +using LogicalSessionRecordSet = stdx::unordered_set<LogicalSessionRecord, LogicalSessionRecordHash>; + +template <typename T> +using LogicalSessionIdMap = stdx::unordered_map<LogicalSessionId, T, LogicalSessionIdHash>; + +class TxnNumberAndRetryCounter { +public: + TxnNumberAndRetryCounter(TxnNumber txnNumber, boost::optional<TxnRetryCounter> txnRetryCounter) + : _txnNumber(txnNumber), _txnRetryCounter(txnRetryCounter) {} + + TxnNumberAndRetryCounter(TxnNumber txnNumber) + : _txnNumber(txnNumber), _txnRetryCounter(boost::none) {} + + BSONObj toBSON() const { + BSONObjBuilder bob; + bob.append(OperationSessionInfo::kTxnNumberFieldName, _txnNumber); + if (_txnRetryCounter) { + bob.append(OperationSessionInfo::kTxnRetryCounterFieldName, *_txnRetryCounter); + } + return bob.obj(); + } + + TxnNumber getTxnNumber() const { + return _txnNumber; + } + + boost::optional<TxnRetryCounter> getTxnRetryCounter() const { + return _txnRetryCounter; + } + + void setTxnNumber(const TxnNumber txnNumber) { + _txnNumber = txnNumber; + } + + void setTxnRetryCounter(const boost::optional<TxnRetryCounter> txnRetryCounter) { + _txnRetryCounter = txnRetryCounter; + } + +private: + TxnNumber _txnNumber; + boost::optional<TxnRetryCounter> _txnRetryCounter; +}; + +inline bool operator==(const TxnNumberAndRetryCounter& l, const TxnNumberAndRetryCounter& r) { + return l.getTxnNumber() == r.getTxnNumber() && l.getTxnRetryCounter() == r.getTxnRetryCounter(); +} + +inline bool operator!=(const TxnNumberAndRetryCounter& l, const TxnNumberAndRetryCounter& r) { + return !(l == r); +} + +} // namespace mongo diff --git a/src/mongo/db/session/logical_session_id.idl b/src/mongo/db/session/logical_session_id.idl new file mode 100644 index 00000000000..85cf3ba1435 --- /dev/null +++ b/src/mongo/db/session/logical_session_id.idl @@ -0,0 +1,245 @@ +# Copyright (C) 2018-present MongoDB, Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the Server Side Public License, version 1, +# as published by MongoDB, Inc. +# +# 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 +# Server Side Public License for more details. +# +# You should have received a copy of the Server Side Public License +# along with this program. If not, see +# <http://www.mongodb.com/licensing/server-side-public-license>. +# +# 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 Server Side 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. +# + +# This IDL file describes the BSON format for a LogicalSessionId, and +# handles the serialization to and deserialization from its BSON representation +# for that class. + +global: + cpp_namespace: "mongo" + cpp_includes: + - "mongo/util/uuid.h" + +imports: + - "mongo/idl/basic_types.idl" + - "mongo/crypto/sha256_block.idl" + +types: + + TxnNumber: + description: "A strictly-increasing per-session counter, which indicates to which transaction + of a given session does the specified command belong. The combination of + LogicalSessionId:TxnNumber is referred to as the transaction identifier." + bson_serialization_type: long + cpp_type: "std::int64_t" + deserializer: "mongo::BSONElement::_numberLong" + + StmtId: + description: "A number representing an operation within a transaction." + bson_serialization_type: int + cpp_type: "std::int32_t" + deserializer: "mongo::BSONElement::_numberInt" + + TxnRetryCounter: + description: "A strictly-increasing per-transaction counter that starts at 0 and is incremented + every time the transaction is internally retried on a transient transaction + error." + bson_serialization_type: int + cpp_type: "std::int32_t" + deserializer: "mongo::BSONElement::_numberInt" + +structs: + LogicalSessionId: + description: "A struct representing a LogicalSessionId" + strict: true + fields: + id: uuid + uid: sha256Block + # The "txnNumber" field must come after the "id" and "uid" fields and before "txnUUID" field + # to enable find commands with session id as the sort field to return results that correspond + # to the same session id sorted by the "txnNumber" field. + txnNumber: + description: "Used for internal sessions only." + type: TxnNumber + optional: true + stability: stable + txnUUID: + description: "Used for internal sessions only." + type: uuid + optional: true + stability: stable + + LogicalSessionIdToClient: + description: "A struct representing a LogicalSessionId to external clients" + strict: true + fields: + id: uuid + + LogicalSessionToClient: + description: "A struct representing a LogicalSession reply to external clients" + strict: true + fields: + id: LogicalSessionIdToClient + timeoutMinutes: int + + LogicalSessionRecord: + description: "A struct representing a LogicalSessionRecord" + strict: true + fields: + _id: + type: LogicalSessionId + cpp_name: id + lastUse: date + user: + type: string + optional: true + + LogicalSessionFromClient: + description: "A struct representing a LogicalSessionId from external clients" + strict: true + fields: + id: + type: uuid + stability: stable + uid: + type: sha256Block + optional: true + stability: stable + txnNumber: + description: "Used for internal sessions only." + type: TxnNumber + optional: true + stability: stable + txnUUID: + description: "Used for internal sessions only." + type: uuid + optional: true + stability: stable + + OperationSessionInfo: + description: "Parser for serializing session transaction metadata" + strict: false + fields: + lsid: + type: LogicalSessionId + cpp_name: sessionId + optional: true + txnNumber: + description: "The transaction number relative to the session in which a particular write + operation executes." + type: TxnNumber + optional: true + txnRetryCounter: + description: "The transaction retry counter for this transaction." + type: TxnRetryCounter + optional: true + autocommit: + type: bool + optional: true + startTransaction: + description: "Used to indicate that a command is the start of a multi-statement + transaction." + type: bool + optional: true + + OperationSessionInfoFromClient: + description: "Parser for pulling out session transaction metadata from commands" + strict: false + fields: + lsid: + type: LogicalSessionFromClient + cpp_name: sessionId + optional: true + txnNumber: + description: "The transaction number relative to the session in which a particular write + operation executes." + type: TxnNumber + optional: true + txnRetryCounter: + description: "The transaction retry counter for this transaction." + type: TxnRetryCounter + optional: true + autocommit: + type: bool + optional: true + startTransaction: + description: "Used to indicate that a command is the start of a multi-statement + transaction." + type: bool + optional: true + coordinator: + description: "Indicates that this shard is the coordinator shard for the transaction two-phase commit." + type: bool + optional: true + + SessionsCollectionFetchResultIndividualResult: + description: "Individual result" + strict: true + fields: + _id: LogicalSessionId + + SessionsCollectionFetchResultCursor: + description: "Cursor object" + strict: false + fields: + firstBatch: array<SessionsCollectionFetchResultIndividualResult> + + SessionsCollectionFetchResult: + description: "Parser for pulling out the fetch results from SessionsCollection::fetch" + strict: false + fields: + cursor: SessionsCollectionFetchResultCursor + + SessionsCollectionFetchRequestFilterId: + description: "Id" + strict: true + fields: + $in: + type: array<LogicalSessionId> + cpp_name: "in" + + SessionsCollectionFetchRequestFilter: + description: "filter" + strict: true + fields: + _id: SessionsCollectionFetchRequestFilterId + + SessionsCollectionFetchRequestProjection: + description: "projection" + strict: true + fields: + _id: int + + SessionsCollectionFetchRequest: + description: "Parser for forming the fetch request for SessionsCollection::fetch" + strict: true + fields: + find: string + filter: SessionsCollectionFetchRequestFilter + projection: SessionsCollectionFetchRequestProjection + batchSize: int + singleBatch: bool + limit: int + +server_parameters: + localLogicalSessionTimeoutMinutes: + description: 'The time in minutes that a session remains active after its most recent use' + set_at: [ startup ] + cpp_vartype: int + cpp_varname: localLogicalSessionTimeoutMinutes + default: 30 diff --git a/src/mongo/db/session/logical_session_id_helpers.cpp b/src/mongo/db/session/logical_session_id_helpers.cpp new file mode 100644 index 00000000000..553443e71d5 --- /dev/null +++ b/src/mongo/db/session/logical_session_id_helpers.cpp @@ -0,0 +1,271 @@ +/** + * Copyright (C) 2018-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * 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 + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * 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 Server Side 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/session/logical_session_id_helpers.h" + +#include "mongo/db/auth/authorization_session.h" +#include "mongo/db/auth/user.h" +#include "mongo/db/auth/user_name.h" +#include "mongo/db/commands/feature_compatibility_version_documentation.h" +#include "mongo/db/operation_context.h" +#include "mongo/db/session/logical_session_cache.h" + +namespace mongo { +namespace { +/** + * If auth is not enabled, will return boost::none. + * Otherwise, a user must be actively authenticated on the client, + * and a handle to this user will be returned. + */ +boost::optional<UserHandle> getAuthenticatedUser(Client* client) { + if (!AuthorizationManager::get(client->getServiceContext())->isAuthEnabled()) { + return boost::none; + } + + auto optUser = AuthorizationSession::get(client)->getAuthenticatedUser(); + uassert(ErrorCodes::Unauthorized, "Logical sessions require authentication", optUser); + + return optUser.value(); +} +} // namespace + +/** + * This is a safe hash that will not collide with a username because all full usernames include an + * '@' character. + */ +const auto kNoAuthDigest = SHA256Block::computeHash(reinterpret_cast<const uint8_t*>(""), 0); + +SHA256Block getLogicalSessionUserDigestForLoggedInUser(const OperationContext* opCtx) { + if (auto user = getAuthenticatedUser(opCtx->getClient())) { + uassert(ErrorCodes::BadValue, + "Username too long to use with logical sessions", + user.value()->getName().getDisplayNameLength() < + kMaximumUserNameLengthForLogicalSessions); + return user.value()->getDigest(); + } else { + return kNoAuthDigest; + } +} + +SHA256Block getLogicalSessionUserDigestFor(StringData user, StringData db) { + if (user.empty() && db.empty()) { + return kNoAuthDigest; + } + const UserName un(user, db); + auto fn = un.getDisplayName(); + return SHA256Block::computeHash({ConstDataRange(fn.c_str(), fn.size())}); +} + +bool isParentSessionId(const LogicalSessionId& sessionId) { + // All child sessions must have a txnUUID. + return !sessionId.getTxnUUID(); +} + +bool isChildSession(const LogicalSessionId& sessionId) { + // All child sessions must have a txnUUID. + return bool(sessionId.getTxnUUID()); +} + +boost::optional<LogicalSessionId> getParentSessionId(const LogicalSessionId& sessionId) { + if (sessionId.getTxnUUID()) { + return LogicalSessionId{sessionId.getId(), sessionId.getUid()}; + } + return boost::none; +} + +LogicalSessionId castToParentSessionId(const LogicalSessionId& sessionId) { + if (auto parentSessionId = getParentSessionId(sessionId)) { + return *parentSessionId; + } + return sessionId; +} + +bool isInternalSessionForRetryableWrite(const LogicalSessionId& sessionId) { + return sessionId.getTxnNumber().has_value(); +} + +bool isDefaultTxnRetryCounter(TxnRetryCounter txnRetryCounter) { + return txnRetryCounter == 0; +} + +bool isInternalSessionForNonRetryableWrite(const LogicalSessionId& sessionId) { + return sessionId.getTxnUUID() && !sessionId.getTxnNumber(); +} + +LogicalSessionId makeLogicalSessionIdWithTxnNumberAndUUID(const LogicalSessionId& parentLsid, + TxnNumber txnNumber) { + auto lsid = LogicalSessionId(parentLsid.getId(), parentLsid.getUid()); + lsid.setTxnNumber(txnNumber); + lsid.setTxnUUID(UUID::gen()); + return lsid; +} + +LogicalSessionId makeLogicalSessionIdWithTxnUUID(const LogicalSessionId& parentLsid) { + auto lsid = LogicalSessionId(parentLsid.getId(), parentLsid.getUid()); + lsid.setTxnUUID(UUID::gen()); + return lsid; +} + +LogicalSessionId makeLogicalSessionId(const LogicalSessionFromClient& fromClient, + OperationContext* opCtx, + std::initializer_list<Privilege> allowSpoof) { + uassert(ErrorCodes::InvalidOptions, + "Cannot specify txnNumber in lsid without specifying txnUUID", + !fromClient.getTxnNumber() || fromClient.getTxnUUID()); + + LogicalSessionId lsid; + + lsid.setId(fromClient.getId()); + lsid.setTxnNumber(fromClient.getTxnNumber()); + lsid.setTxnUUID(fromClient.getTxnUUID()); + + if (fromClient.getUid()) { + auto authSession = AuthorizationSession::get(opCtx->getClient()); + + uassert(ErrorCodes::Unauthorized, + "Unauthorized to set user digest in LogicalSessionId", + std::any_of(allowSpoof.begin(), + allowSpoof.end(), + [&](const auto& priv) { + return authSession->isAuthorizedForPrivilege(priv); + }) || + authSession->isAuthorizedForPrivilege(Privilege( + ResourcePattern::forClusterResource(), ActionType::impersonate)) || + getLogicalSessionUserDigestForLoggedInUser(opCtx) == fromClient.getUid()); + + lsid.setUid(*fromClient.getUid()); + } else { + lsid.setUid(getLogicalSessionUserDigestForLoggedInUser(opCtx)); + } + + return lsid; +} + +LogicalSessionId makeLogicalSessionId(OperationContext* opCtx) { + LogicalSessionId id{}; + + id.setId(UUID::gen()); + id.setUid(getLogicalSessionUserDigestForLoggedInUser(opCtx)); + + return id; +} + +LogicalSessionId makeSystemLogicalSessionId() { + LogicalSessionId id{}; + + id.setId(UUID::gen()); + id.setUid((*internalSecurity.getUser())->getDigest()); + + return id; +} + +LogicalSessionRecord makeLogicalSessionRecord(OperationContext* opCtx, Date_t lastUse) { + LogicalSessionId id{}; + LogicalSessionRecord lsr{}; + + if (auto user = getAuthenticatedUser(opCtx->getClient())) { + id.setUid(user.value()->getDigest()); + lsr.setUser(StringData(user.value()->getName().getDisplayName())); + } else { + id.setUid(kNoAuthDigest); + } + + id.setId(UUID::gen()); + + lsr.setId(id); + lsr.setLastUse(lastUse); + + return lsr; +} + +LogicalSessionRecord makeLogicalSessionRecord(const LogicalSessionId& lsid, Date_t lastUse) { + LogicalSessionId id{}; + LogicalSessionRecord lsr{}; + + id.setId(lsid.getId()); + id.setUid(lsid.getUid()); + + lsr.setId(id); + lsr.setLastUse(lastUse); + + return lsr; +} + +LogicalSessionRecord makeLogicalSessionRecord(OperationContext* opCtx, + const LogicalSessionId& lsid, + Date_t lastUse) { + auto lsr = makeLogicalSessionRecord(lsid, lastUse); + + if (auto user = getAuthenticatedUser(opCtx->getClient())) { + if (user.value()->getDigest() == lsid.getUid()) { + lsr.setUser(StringData(user.value()->getName().getDisplayName())); + } + } + + return lsr; +} + + +LogicalSessionToClient makeLogicalSessionToClient(const LogicalSessionId& lsid) { + LogicalSessionIdToClient lsitc; + lsitc.setId(lsid.getId()); + + LogicalSessionToClient id; + + id.setId(lsitc); + id.setTimeoutMinutes(localLogicalSessionTimeoutMinutes); + + return id; +}; + +LogicalSessionIdSet makeLogicalSessionIds(const std::vector<LogicalSessionFromClient>& sessions, + OperationContext* opCtx, + std::initializer_list<Privilege> allowSpoof) { + LogicalSessionIdSet lsids; + lsids.reserve(sessions.size()); + for (auto&& session : sessions) { + lsids.emplace(makeLogicalSessionId(session, opCtx, allowSpoof)); + } + + return lsids; +} + +namespace logical_session_id_helpers { + +void serializeLsidAndTxnNumber(OperationContext* opCtx, BSONObjBuilder* builder) { + OperationSessionInfo sessionInfo; + sessionInfo.setSessionId(opCtx->getLogicalSessionId()); + sessionInfo.setTxnNumber(opCtx->getTxnNumber()); + sessionInfo.serialize(builder); +} + +} // namespace logical_session_id_helpers +} // namespace mongo diff --git a/src/mongo/db/session/logical_session_id_helpers.h b/src/mongo/db/session/logical_session_id_helpers.h new file mode 100644 index 00000000000..cb140d292ed --- /dev/null +++ b/src/mongo/db/session/logical_session_id_helpers.h @@ -0,0 +1,135 @@ +/** + * Copyright (C) 2018-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * 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 + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * 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 Server Side 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 <initializer_list> +#include <vector> + +#include "mongo/db/auth/privilege.h" +#include "mongo/db/session/logical_session_id.h" + +namespace mongo { + +constexpr size_t kMaximumUserNameLengthForLogicalSessions = 10000; + +/** + * Get the currently logged in user's UID digest. + */ +SHA256Block getLogicalSessionUserDigestForLoggedInUser(const OperationContext* opCtx); + +/** + * Get a user digest for a specific user/db identifier. + */ +SHA256Block getLogicalSessionUserDigestFor(StringData user, StringData db); + +/** + * Returns if the given session is a parent session, ie only has fields that could have come from an + * external client. + */ +bool isParentSessionId(const LogicalSessionId& sessionId); + +/** + * Returns if the given session is a child session, ie it was created on behalf of an operation that + * already had a session. + */ +bool isChildSession(const LogicalSessionId& sessionId); + +/** + * Returns the parent session id for the given session id if there is one. + */ +boost::optional<LogicalSessionId> getParentSessionId(const LogicalSessionId& sessionId); + +/** + * Returns the upconverted parent session id for the given session id if there is one. + * Otherwise, returns the session id itself. + */ +LogicalSessionId castToParentSessionId(const LogicalSessionId& sessionId); + +/** + * Returns true if the session with the given session id is an internal session for internal + * transactions for retryable writes. + */ +bool isInternalSessionForRetryableWrite(const LogicalSessionId& sessionId); + +/** + * Returns true if the txnRetryCounter is still the default value, meaning that for the relevant + * internal transaction, no retries have occurred. + */ +bool isDefaultTxnRetryCounter(TxnRetryCounter txnRetryCounter); + +/** + * Returns true if the session with the given session id is an internal session for internal + * transactions for non-retryable writes (i.e. writes in a session without a transaction number). + */ +bool isInternalSessionForNonRetryableWrite(const LogicalSessionId& sessionId); + +/** + * Helpers to make internal sessions. + */ +LogicalSessionId makeLogicalSessionIdWithTxnNumberAndUUID(const LogicalSessionId& parentLsid, + TxnNumber txnNumber); +LogicalSessionId makeLogicalSessionIdWithTxnUUID(const LogicalSessionId& parentLsid); + +/** + * Factory functions to generate logical session records. + */ +LogicalSessionId makeLogicalSessionId(const LogicalSessionFromClient& lsid, + OperationContext* opCtx, + std::initializer_list<Privilege> allowSpoof = {}); +LogicalSessionId makeLogicalSessionId(OperationContext* opCtx); + +/** + * We recommend acquiring a system session through the session pool. It can be acquired through this + * method InternalSessionPool::acquireSystemSession(). + */ +LogicalSessionId makeSystemLogicalSessionId(); + +/** + * Factory functions to make logical session records. The overloads that + * take an OperationContext should be used when possible, as they will also set the + * user information on the record. + */ +LogicalSessionRecord makeLogicalSessionRecord(const LogicalSessionId& lsid, Date_t lastUse); +LogicalSessionRecord makeLogicalSessionRecord(OperationContext* opCtx, Date_t lastUse); +LogicalSessionRecord makeLogicalSessionRecord(OperationContext* opCtx, + const LogicalSessionId& lsid, + Date_t lastUse); + +LogicalSessionToClient makeLogicalSessionToClient(const LogicalSessionId& lsid); +LogicalSessionIdSet makeLogicalSessionIds(const std::vector<LogicalSessionFromClient>& sessions, + OperationContext* opCtx, + std::initializer_list<Privilege> allowSpoof = {}); + +namespace logical_session_id_helpers { + +void serializeLsidAndTxnNumber(OperationContext* opCtx, BSONObjBuilder* builder); + +} // namespace logical_session_id_helpers +} // namespace mongo diff --git a/src/mongo/db/session/logical_session_id_test.cpp b/src/mongo/db/session/logical_session_id_test.cpp new file mode 100644 index 00000000000..736525ecf0f --- /dev/null +++ b/src/mongo/db/session/logical_session_id_test.cpp @@ -0,0 +1,451 @@ +/** + * Copyright (C) 2018-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * 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 + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * 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 Server Side 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 <string> + +#include "mongo/db/session/logical_session_id.h" + +#include "mongo/crypto/mechanism_scram.h" +#include "mongo/crypto/sha1_block.h" +#include "mongo/crypto/sha256_block.h" +#include "mongo/db/auth/action_set.h" +#include "mongo/db/auth/action_type.h" +#include "mongo/db/auth/authorization_manager.h" +#include "mongo/db/auth/authorization_manager_impl.h" +#include "mongo/db/auth/authorization_session.h" +#include "mongo/db/auth/authz_manager_external_state_mock.h" +#include "mongo/db/auth/authz_session_external_state_mock.h" +#include "mongo/db/auth/sasl_options.h" +#include "mongo/db/auth/user.h" +#include "mongo/db/jsobj.h" +#include "mongo/db/operation_context.h" +#include "mongo/db/service_context.h" +#include "mongo/db/service_context_test_fixture.h" +#include "mongo/db/service_liaison_mock.h" +#include "mongo/db/session/initialize_operation_session_info.h" +#include "mongo/db/session/logical_session_cache.h" +#include "mongo/db/session/logical_session_cache_impl.h" +#include "mongo/db/session/logical_session_id_helpers.h" +#include "mongo/db/session/sessions_collection_mock.h" +#include "mongo/transport/session.h" +#include "mongo/transport/transport_layer_mock.h" +#include "mongo/unittest/unittest.h" + +namespace mongo { +namespace { + +class LogicalSessionIdTest : public ServiceContextTest { +public: + AuthzManagerExternalStateMock* managerState; + transport::TransportLayerMock transportLayer; + transport::SessionHandle session = transportLayer.createSession(); + ServiceContext::UniqueOperationContext _opCtx; + AuthorizationSession* authzSession; + + LogicalSessionIdTest() { + RestrictionEnvironment::set( + session, std::make_unique<RestrictionEnvironment>(SockAddr(), SockAddr())); + auto localManagerState = std::make_unique<AuthzManagerExternalStateMock>(); + managerState = localManagerState.get(); + managerState->setAuthzVersion(AuthorizationManager::schemaVersion26Final); + auto authzManager = std::make_unique<AuthorizationManagerImpl>( + getServiceContext(), std::move(localManagerState)); + authzManager->setAuthEnabled(true); + AuthorizationManager::set(getServiceContext(), std::move(authzManager)); + Client::releaseCurrent(); + Client::initThread(getThreadName(), session); + authzSession = AuthorizationSession::get(getClient()); + _opCtx = makeOperationContext(); + + auto localServiceLiaison = + std::make_unique<MockServiceLiaison>(std::make_shared<MockServiceLiaisonImpl>()); + auto localSessionsCollection = std::make_unique<MockSessionsCollection>( + std::make_shared<MockSessionsCollectionImpl>()); + + auto localLogicalSessionCache = std::make_unique<LogicalSessionCacheImpl>( + std::move(localServiceLiaison), + std::move(localSessionsCollection), + [](OperationContext*, SessionsCollection&, Date_t) { + return 0; /* No op*/ + }); + + LogicalSessionCache::set(getServiceContext(), std::move(localLogicalSessionCache)); + } + + User* addSimpleUser(UserName un) { + const auto creds = BSON("SCRAM-SHA-1" << scram::Secrets<SHA1Block>::generateCredentials( + "a", saslGlobalParams.scramSHA1IterationCount.load())); + ASSERT_OK(managerState->insertPrivilegeDocument( + _opCtx.get(), + BSON("user" << un.getUser() << "db" << un.getDB() << "credentials" << creds << "roles" + << BSON_ARRAY(BSON("role" + << "readWrite" + << "db" + << "test"))), + BSONObj())); + ASSERT_OK(authzSession->addAndAuthorizeUser(_opCtx.get(), un)); + return authzSession->lookupUser(un); + } + + User* addClusterUser(UserName un) { + const auto creds = BSON("SCRAM-SHA-256" << scram::Secrets<SHA256Block>::generateCredentials( + "a", saslGlobalParams.scramSHA256IterationCount.load())); + ASSERT_OK(managerState->insertPrivilegeDocument( + _opCtx.get(), + BSON("user" << un.getUser() << "db" << un.getDB() << "credentials" << creds << "roles" + << BSON_ARRAY(BSON("role" + << "__system" + << "db" + << "admin"))), + BSONObj())); + ASSERT_OK(authzSession->addAndAuthorizeUser(_opCtx.get(), un)); + return authzSession->lookupUser(un); + } +}; + +TEST_F(LogicalSessionIdTest, ConstructorFromClientWithoutPassedUid) { + auto id = UUID::gen(); + User* user = addSimpleUser(UserName("simple", "test")); + + LogicalSessionFromClient req; + req.setId(id); + + LogicalSessionId lsid = makeLogicalSessionId(req, _opCtx.get()); + ASSERT_EQ(lsid.getId(), id); + ASSERT_EQ(lsid.getUid(), user->getDigest()); +} + +TEST_F(LogicalSessionIdTest, ConstructorFromClientWithTxnUUID) { + auto id = UUID::gen(); + auto txnUUID = UUID::gen(); + User* user = addSimpleUser(UserName("simple", "test")); + + LogicalSessionFromClient req; + req.setId(id); + req.setTxnUUID(txnUUID); + + LogicalSessionId lsid = makeLogicalSessionId(req, _opCtx.get()); + ASSERT_EQ(lsid.getId(), id); + ASSERT_EQ(lsid.getUid(), user->getDigest()); + ASSERT_EQ(*lsid.getTxnUUID(), txnUUID); +} + +TEST_F(LogicalSessionIdTest, ConstructorFromClientWithTxnNumberWithoutTxnUUID) { + auto id = UUID::gen(); + TxnNumber txnNumber(35); + addSimpleUser(UserName("simple", "test")); + + LogicalSessionFromClient req; + req.setId(id); + req.setTxnNumber(txnNumber); + + ASSERT_THROWS_CODE( + makeLogicalSessionId(req, _opCtx.get()), DBException, ErrorCodes::InvalidOptions); +} + +TEST_F(LogicalSessionIdTest, ConstructorFromClientWithTxnNumberAndTxnUUID) { + auto id = UUID::gen(); + TxnNumber txnNumber(35); + auto txnUUID = UUID::gen(); + User* user = addSimpleUser(UserName("simple", "test")); + + LogicalSessionFromClient req; + req.setId(id); + req.setTxnNumber(txnNumber); + req.setTxnUUID(txnUUID); + + LogicalSessionId lsid = makeLogicalSessionId(req, _opCtx.get()); + ASSERT_EQ(lsid.getId(), id); + ASSERT_EQ(lsid.getUid(), user->getDigest()); + ASSERT_EQ(*lsid.getTxnNumber(), txnNumber); + ASSERT_EQ(*lsid.getTxnUUID(), txnUUID); +} + +TEST_F(LogicalSessionIdTest, ConstructorFromClientWithoutPassedUidAndWithoutAuthedUser) { + auto id = UUID::gen(); + + LogicalSessionFromClient req; + req.setId(id); + + ASSERT_THROWS(makeLogicalSessionId(req, _opCtx.get()), AssertionException); +} + +TEST_F(LogicalSessionIdTest, ConstructorFromClientWithPassedUidWithPermissions) { + auto id = UUID::gen(); + auto uid = SHA256Block{}; + addClusterUser(UserName("cluster", "test")); + + LogicalSessionFromClient req; + req.setId(id); + req.setUid(uid); + + LogicalSessionId lsid = makeLogicalSessionId(req, _opCtx.get()); + + ASSERT_EQ(lsid.getId(), id); + ASSERT_EQ(lsid.getUid(), uid); +} + +TEST_F(LogicalSessionIdTest, ConstructorFromClientWithOwnUidWithNonImpersonatePermissions) { + User* user = addSimpleUser(UserName("simple", "test")); + auto id = UUID::gen(); + auto uid = user->getDigest(); + + LogicalSessionFromClient req; + req.setId(id); + req.setUid(uid); + + LogicalSessionId lsid = makeLogicalSessionId(req, _opCtx.get()); + ASSERT_EQ(lsid.getId(), id); + ASSERT_EQ(lsid.getUid(), uid); +} + +TEST_F(LogicalSessionIdTest, ConstructorFromClientWithPassedUidWithoutAuthedUser) { + auto id = UUID::gen(); + auto uid = SHA256Block{}; + + LogicalSessionFromClient req; + req.setId(id); + req.setUid(uid); + + ASSERT_THROWS(makeLogicalSessionId(req, _opCtx.get()), AssertionException); +} + +TEST_F(LogicalSessionIdTest, ConstructorFromClientWithPassedNonMatchingUidWithoutPermissions) { + auto id = UUID::gen(); + auto uid = SHA256Block{}; + addSimpleUser(UserName("simple", "test")); + + LogicalSessionFromClient req; + req.setId(id); + req.setUid(uid); + + ASSERT_THROWS(makeLogicalSessionId(req, _opCtx.get()), AssertionException); +} + +TEST_F(LogicalSessionIdTest, ConstructorFromClientWithPassedMatchingUidWithoutPermissions) { + auto id = UUID::gen(); + User* user = addSimpleUser(UserName("simple", "test")); + auto uid = user->getDigest(); + + LogicalSessionFromClient req; + req.setId(id); + req.setUid(uid); + + LogicalSessionId lsid = makeLogicalSessionId(req, _opCtx.get()); + + ASSERT_EQ(lsid.getId(), id); + ASSERT_EQ(lsid.getUid(), uid); +} + +TEST_F(LogicalSessionIdTest, GenWithUser) { + User* user = addSimpleUser(UserName("simple", "test")); + auto lsid = makeLogicalSessionId(_opCtx.get()); + + ASSERT_EQ(lsid.getUid(), user->getDigest()); +} + +TEST_F(LogicalSessionIdTest, GenWithoutAuthedUser) { + ASSERT_THROWS(makeLogicalSessionId(_opCtx.get()), AssertionException); +} + +TEST_F(LogicalSessionIdTest, InitializeOperationSessionInfo_NoSessionIdNoTransactionNumber) { + addSimpleUser(UserName("simple", "test")); + initializeOperationSessionInfo(_opCtx.get(), BSON("TestCmd" << 1), true, true, true); + + ASSERT(!_opCtx->getLogicalSessionId()); + ASSERT(!_opCtx->getTxnNumber()); +} + +TEST_F(LogicalSessionIdTest, InitializeOperationSessionInfo_SessionIdNoTransactionNumber) { + addSimpleUser(UserName("simple", "test")); + LogicalSessionFromClient lsid{}; + lsid.setId(UUID::gen()); + + initializeOperationSessionInfo(_opCtx.get(), + BSON("TestCmd" << 1 << "lsid" << lsid.toBSON() << "OtherField" + << "TestField"), + true, + true, + true); + + ASSERT(_opCtx->getLogicalSessionId()); + ASSERT_EQ(lsid.getId(), _opCtx->getLogicalSessionId()->getId()); + + ASSERT(!_opCtx->getTxnNumber()); +} + +TEST_F(LogicalSessionIdTest, InitializeOperationSessionInfo_MissingSessionIdWithTransactionNumber) { + addSimpleUser(UserName("simple", "test")); + ASSERT_THROWS_CODE( + initializeOperationSessionInfo(_opCtx.get(), + BSON("TestCmd" << 1 << "txnNumber" << 100LL << "OtherField" + << "TestField"), + true, + true, + true), + AssertionException, + ErrorCodes::InvalidOptions); +} + +TEST_F(LogicalSessionIdTest, InitializeOperationSessionInfo_SessionIdAndTransactionNumber) { + addSimpleUser(UserName("simple", "test")); + LogicalSessionFromClient lsid; + lsid.setId(UUID::gen()); + + initializeOperationSessionInfo(_opCtx.get(), + BSON("TestCmd" << 1 << "lsid" << lsid.toBSON() << "txnNumber" + << 100LL << "OtherField" + << "TestField"), + true, + true, + true); + + ASSERT(_opCtx->getLogicalSessionId()); + ASSERT_EQ(lsid.getId(), _opCtx->getLogicalSessionId()->getId()); + + ASSERT(_opCtx->getTxnNumber()); + ASSERT_EQ(100, *_opCtx->getTxnNumber()); +} + +TEST_F(LogicalSessionIdTest, InitializeOperationSessionInfo_IsReplSetMemberOrMongosFalse) { + addSimpleUser(UserName("simple", "test")); + LogicalSessionFromClient lsid; + lsid.setId(UUID::gen()); + + ASSERT_THROWS_CODE( + initializeOperationSessionInfo(_opCtx.get(), + BSON("TestCmd" << 1 << "lsid" << lsid.toBSON() << "txnNumber" + << 100LL << "OtherField" + << "TestField"), + true, + true, + false), + AssertionException, + ErrorCodes::IllegalOperation); +} + +TEST_F(LogicalSessionIdTest, InitializeOperationSessionInfo_IgnoresInfoIfNoCache) { + addSimpleUser(UserName("simple", "test")); + LogicalSessionFromClient lsid; + lsid.setId(UUID::gen()); + + LogicalSessionCache::set(_opCtx->getServiceContext(), nullptr); + + auto sessionInfo = initializeOperationSessionInfo( + _opCtx.get(), + BSON("TestCmd" << 1 << "lsid" << lsid.toBSON() << "txnNumber" << 100LL << "OtherField" + << "TestField"), + true, + true, + true); + ASSERT(sessionInfo.getSessionId() == boost::none); + ASSERT(sessionInfo.getTxnNumber() == boost::none); + ASSERT(sessionInfo.getStartTransaction() == boost::none); + ASSERT(sessionInfo.getAutocommit() == boost::none); +} + +TEST_F(LogicalSessionIdTest, InitializeOperationSessionInfo_IgnoresInfoIfDoNotAttachToOpCtx) { + addSimpleUser(UserName("simple", "test")); + LogicalSessionFromClient lsid; + lsid.setId(UUID::gen()); + + auto sessionInfo = initializeOperationSessionInfo( + _opCtx.get(), + BSON("TestCmd" << 1 << "lsid" << lsid.toBSON() << "txnNumber" << 100LL << "OtherField" + << "TestField"), + true, + false, + true); + + ASSERT(sessionInfo.getSessionId() == boost::none); + ASSERT(sessionInfo.getTxnNumber() == boost::none); + ASSERT(sessionInfo.getStartTransaction() == boost::none); + ASSERT(sessionInfo.getAutocommit() == boost::none); + + ASSERT(_opCtx->getLogicalSessionId() == boost::none); + ASSERT(_opCtx->getTxnNumber() == boost::none); +} + +TEST_F(LogicalSessionIdTest, InitializeOperationSessionInfo_VerifyUIDEvenIfDoNotAttachToOpCtx) { + addSimpleUser(UserName("simple", "test")); + LogicalSessionFromClient lsid; + lsid.setId(UUID::gen()); + + auto invalidDigest = SHA256Block::computeHash({ConstDataRange("hacker")}); + lsid.setUid(invalidDigest); + + ASSERT_THROWS_CODE(initializeOperationSessionInfo( + _opCtx.get(), + BSON("TestCmd" << 1 << "lsid" << lsid.toBSON() << "txnNumber" << 100LL), + true, + false, + true), + AssertionException, + ErrorCodes::Unauthorized); +} + +TEST_F(LogicalSessionIdTest, InitializeOperationSessionInfo_SendingInfoFailsInDirectClient) { + const std::vector<BSONObj> operationSessionParameters{ + {BSON("lsid" << makeLogicalSessionIdForTest().toBSON())}, + {BSON("txnNumber" << 1LL)}, + {BSON("autocommit" << true)}, + {BSON("startTransaction" << true)}}; + + + _opCtx->getClient()->setInDirectClient(true); + + for (const auto& param : operationSessionParameters) { + BSONObjBuilder commandBuilder = BSON("count" + << "foo"); + commandBuilder.appendElements(param); + + ASSERT_THROWS_CODE( + initializeOperationSessionInfo(_opCtx.get(), commandBuilder.obj(), true, true, true), + AssertionException, + 50891); + } + + _opCtx->getClient()->setInDirectClient(false); +} + +TEST_F(LogicalSessionIdTest, ConstructorFromClientWithTooLongName) { + auto id = UUID::gen(); + + addSimpleUser(UserName(std::string(kMaximumUserNameLengthForLogicalSessions + 1, 'x'), "test")); + + LogicalSessionFromClient req; + req.setId(id); + + ASSERT_THROWS(makeLogicalSessionId(req, _opCtx.get()), AssertionException); +} + +} // namespace +} // namespace mongo diff --git a/src/mongo/db/session/session.h b/src/mongo/db/session/session.h new file mode 100644 index 00000000000..471d253aac4 --- /dev/null +++ b/src/mongo/db/session/session.h @@ -0,0 +1,79 @@ +/** + * Copyright (C) 2018-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * 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 + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * 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 Server Side 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 "mongo/db/operation_context.h" +#include "mongo/db/session/logical_session_id.h" +#include "mongo/util/concurrency/with_lock.h" +#include "mongo/util/decorable.h" + +namespace mongo { + +/** + * A decorable container for state associated with an active transaction session running on a MongoD + * or MongoS server. Refer to SessionCatalog for more information on the semantics of sessions. + */ +class Session : public Decorable<Session> { + Session(const Session&) = delete; + Session& operator=(const Session&) = delete; + + friend class ObservableSession; + friend class SessionCatalog; + +public: + explicit Session(LogicalSessionId sessionId) : _sessionId(std::move(sessionId)) {} + + ~Session() { + invariant(!_numWaitingToCheckOut); + } + + const LogicalSessionId& getSessionId() const { + return _sessionId; + } + + Session* getParentSession() const { + return _parentSession; + } + +private: + // The session id of the transaction session that this object represents. + const LogicalSessionId _sessionId; + + // A pointer to the parent Session for this Session if there is one. Set at construction for + // child sessions. Children and parents are reaped atomically, so this pointer should always be + // valid if it is not null. + Session* _parentSession{nullptr}; + + // Counts how many threads are blocked waiting for this Session to become available. Used to + // block reaping of this Session from the SessionCatalog. + int _numWaitingToCheckOut{0}; +}; + +} // namespace mongo diff --git a/src/mongo/db/session/session_catalog.cpp b/src/mongo/db/session/session_catalog.cpp new file mode 100644 index 00000000000..f3d643a70f6 --- /dev/null +++ b/src/mongo/db/session/session_catalog.cpp @@ -0,0 +1,556 @@ +/** + * Copyright (C) 2018-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * 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 + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * 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 Server Side 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/session/session_catalog.h" + +#include <memory> + +#include "mongo/db/internal_transactions_feature_flag_gen.h" +#include "mongo/db/server_options.h" +#include "mongo/db/service_context.h" +#include "mongo/db/session/logical_session_id_helpers.h" +#include "mongo/logv2/log.h" + +#define MONGO_LOGV2_DEFAULT_COMPONENT ::mongo::logv2::LogComponent::kWrite + + +namespace mongo { +namespace { + +const auto sessionTransactionTableDecoration = ServiceContext::declareDecoration<SessionCatalog>(); + +const auto operationSessionDecoration = + OperationContext::declareDecoration<boost::optional<SessionCatalog::ScopedCheckedOutSession>>(); + +MONGO_FAIL_POINT_DEFINE(hangAfterIncrementingNumWaitingToCheckOut); + +} // namespace + +SessionCatalog::~SessionCatalog() { + stdx::lock_guard<Latch> lg(_mutex); + for (const auto& [_, sri] : _sessions) { + ObservableSession osession(lg, sri.get(), &sri->parentSession); + invariant(!osession.hasCurrentOperation()); + invariant(!osession._killed()); + } +} + +void SessionCatalog::reset_forTest() { + stdx::lock_guard<Latch> lg(_mutex); + _sessions.clear(); +} + +SessionCatalog* SessionCatalog::get(OperationContext* opCtx) { + return get(opCtx->getServiceContext()); +} + +SessionCatalog* SessionCatalog::get(ServiceContext* service) { + auto& sessionTransactionTable = sessionTransactionTableDecoration(service); + return &sessionTransactionTable; +} + +SessionCatalog::ScopedCheckedOutSession SessionCatalog::_checkOutSessionInner( + OperationContext* opCtx, const LogicalSessionId& lsid, boost::optional<KillToken> killToken) { + if (killToken) { + dassert(killToken->lsidToKill == lsid); + } else { + dassert(opCtx->getLogicalSessionId() == lsid); + } + + stdx::unique_lock<Latch> ul(_mutex); + + auto sri = _getOrCreateSessionRuntimeInfo(ul, lsid); + auto session = sri->getSession(ul, lsid); + invariant(session); + + if (killToken) { + invariant(ObservableSession(ul, sri, session)._killed()); + } + + // Wait until the session is no longer checked out and until the previously scheduled kill has + // completed. + ++session->_numWaitingToCheckOut; + ON_BLOCK_EXIT([&] { --session->_numWaitingToCheckOut; }); + + if (MONGO_unlikely(hangAfterIncrementingNumWaitingToCheckOut.shouldFail())) { + ul.unlock(); + hangAfterIncrementingNumWaitingToCheckOut.pauseWhileSet(opCtx); + ul.lock(); + } + + opCtx->waitForConditionOrInterrupt( + sri->availableCondVar, ul, [&ul, &sri, &session, forKill = killToken.has_value()]() { + ObservableSession osession(ul, sri, session); + return osession._isAvailableForCheckOut(forKill); + }); + + sri->checkoutOpCtx = opCtx; + sri->lastCheckout = Date_t::now(); + + return ScopedCheckedOutSession(*this, std::move(sri), session, std::move(killToken)); +} + +SessionCatalog::ScopedCheckedOutSession SessionCatalog::_checkOutSession(OperationContext* opCtx) { + // This method is not supposed to be called with an already checked-out session due to risk of + // deadlock + invariant(opCtx->getLogicalSessionId()); + invariant(!operationSessionDecoration(opCtx)); + invariant(!opCtx->lockState()->inAWriteUnitOfWork()); + invariant(!opCtx->lockState()->isLocked()); + + auto lsid = *opCtx->getLogicalSessionId(); + return _checkOutSessionInner(opCtx, lsid, boost::none /* killToken */); +} + +SessionCatalog::SessionToKill SessionCatalog::checkOutSessionForKill(OperationContext* opCtx, + KillToken killToken) { + // This method is not supposed to be called with an already checked-out session due to risk of + // deadlock + invariant(!operationSessionDecoration(opCtx)); + invariant(!opCtx->getTxnNumber()); + + auto lsid = killToken.lsidToKill; + return SessionToKill(_checkOutSessionInner(opCtx, lsid, std::move(killToken))); +} + +void SessionCatalog::scanSession(const LogicalSessionId& lsid, + const ScanSessionsCallbackFn& workerFn, + ScanSessionCreateSession createSession) { + stdx::lock_guard<Latch> lg(_mutex); + + auto sri = (createSession == ScanSessionCreateSession::kYes) + ? _getOrCreateSessionRuntimeInfo(lg, lsid) + : _getSessionRuntimeInfo(lg, lsid); + + if (sri) { + auto session = sri->getSession(lg, lsid); + invariant(session); + + ObservableSession osession(lg, sri, session); + workerFn(osession); + invariant(!osession._markedForReap, "Cannot reap a session via 'scanSession'"); + } +} + +void SessionCatalog::scanSessions(const SessionKiller::Matcher& matcher, + const ScanSessionsCallbackFn& workerFn) { + stdx::lock_guard<Latch> lg(_mutex); + + LOGV2_DEBUG(21976, + 2, + "Scanning {sessionCount} sessions", + "Scanning sessions", + "sessionCount"_attr = _sessions.size()); + + for (auto& [parentLsid, sri] : _sessions) { + if (matcher.match(parentLsid)) { + ObservableSession osession(lg, sri.get(), &sri->parentSession); + workerFn(osession); + invariant(!osession._markedForReap, "Cannot reap a session via 'scanSessions'"); + } + + for (auto& [childLsid, session] : sri->childSessions) { + if (matcher.match(childLsid)) { + ObservableSession osession(lg, sri.get(), &session); + workerFn(osession); + invariant(!osession._markedForReap, "Cannot reap a session via 'scanSessions'"); + } + } + } +} + +void SessionCatalog::scanParentSessions(const ScanSessionsCallbackFn& workerFn) { + stdx::lock_guard<Latch> lg(_mutex); + + LOGV2_DEBUG(6685000, + 2, + "Scanning {sessionCount} sessions", + "Scanning sessions", + "sessionCount"_attr = _sessions.size()); + + for (auto& [parentLsid, sri] : _sessions) { + ObservableSession osession(lg, sri.get(), &sri->parentSession); + workerFn(osession); + invariant(!osession._markedForReap, "Cannot reap a session via 'scanSessions'"); + } +} + +LogicalSessionIdSet SessionCatalog::scanSessionsForReap( + const LogicalSessionId& parentLsid, + const ScanSessionsCallbackFn& parentSessionWorkerFn, + const ScanSessionsCallbackFn& childSessionWorkerFn) { + invariant(isParentSessionId(parentLsid)); + + std::unique_ptr<SessionRuntimeInfo> sriToReap; + { + stdx::lock_guard<Latch> lg(_mutex); + + auto sriIt = _sessions.find(parentLsid); + // The reaper should never try to reap a non-existent session id. + invariant(sriIt != _sessions.end()); + auto sri = sriIt->second.get(); + + LogicalSessionIdSet remainingSessions; + bool shouldReapRemaining = true; + + { + ObservableSession osession(lg, sri, &sri->parentSession); + parentSessionWorkerFn(osession); + + remainingSessions.insert(osession.getSessionId()); + shouldReapRemaining = osession._shouldBeReaped(); + } + + { + auto childSessionIt = sri->childSessions.begin(); + while (childSessionIt != sri->childSessions.end()) { + ObservableSession osession(lg, sri, &childSessionIt->second); + childSessionWorkerFn(osession); + + if (osession._shouldBeReaped() && + (osession._reapMode == ObservableSession::ReapMode::kExclusive)) { + sri->childSessions.erase(childSessionIt++); + continue; + } + + remainingSessions.insert(osession.getSessionId()); + shouldReapRemaining &= osession._shouldBeReaped(); + ++childSessionIt; + } + } + + if (shouldReapRemaining) { + sriToReap = std::move(sriIt->second); + _sessions.erase(sriIt); + remainingSessions.clear(); + } + + return remainingSessions; + } +} + +SessionCatalog::KillToken SessionCatalog::killSession(const LogicalSessionId& lsid) { + stdx::lock_guard<Latch> lg(_mutex); + + auto sri = _getSessionRuntimeInfo(lg, lsid); + uassert(ErrorCodes::NoSuchSession, "Session not found", sri); + auto session = sri->getSession(lg, lsid); + uassert(ErrorCodes::NoSuchSession, "Session not found", session); + return ObservableSession(lg, sri, session).kill(); +} + +size_t SessionCatalog::size() const { + stdx::lock_guard<Latch> lg(_mutex); + return _sessions.size(); +} + +SessionCatalog::SessionRuntimeInfo* SessionCatalog::_getSessionRuntimeInfo( + WithLock wl, const LogicalSessionId& lsid) { + const auto& parentLsid = isParentSessionId(lsid) ? lsid : *getParentSessionId(lsid); + auto sriIt = _sessions.find(parentLsid); + + if (sriIt == _sessions.end()) { + return nullptr; + } + + auto sri = sriIt->second.get(); + auto session = sri->getSession(wl, lsid); + + if (session) { + return sri; + } + + return nullptr; +} + +SessionCatalog::SessionRuntimeInfo* SessionCatalog::_getOrCreateSessionRuntimeInfo( + WithLock lk, const LogicalSessionId& lsid) { + if (auto sri = _getSessionRuntimeInfo(lk, lsid)) { + return sri; + } + + const auto& parentLsid = isParentSessionId(lsid) ? lsid : *getParentSessionId(lsid); + auto sriIt = + _sessions.emplace(parentLsid, std::make_unique<SessionRuntimeInfo>(parentLsid)).first; + auto sri = sriIt->second.get(); + + if (isChildSession(lsid)) { + auto [childSessionIt, inserted] = sri->childSessions.try_emplace(lsid, lsid); + // Insert should always succeed since the session did not exist prior to this. + invariant(inserted); + + auto& childSession = childSessionIt->second; + childSession._parentSession = &sri->parentSession; + } + + return sri; +} + +void SessionCatalog::_releaseSession(SessionRuntimeInfo* sri, + Session* session, + boost::optional<KillToken> killToken, + boost::optional<TxnNumber> clientTxnNumberStarted) { + stdx::unique_lock<Latch> ul(_mutex); + + // Make sure we have exactly the same session on the map and that it is still associated with an + // operation context (meaning checked-out) + invariant(_sessions[sri->parentSession.getSessionId()].get() == sri); + invariant(sri->checkoutOpCtx); + if (killToken) { + dassert(killToken->lsidToKill == session->getSessionId()); + } + + ServiceContext* service = sri->checkoutOpCtx->getServiceContext(); + + sri->checkoutOpCtx = nullptr; + sri->availableCondVar.notify_all(); + + if (killToken) { + invariant(sri->killsRequested > 0); + --sri->killsRequested; + } + + std::vector<LogicalSessionId> eagerlyReapedSessions; + if (clientTxnNumberStarted.has_value()) { + // Since the given txnNumber successfully started, we know any child sessions with older + // txnNumbers can be discarded. This needed to wait until a transaction started because that + // can fail, e.g. if the active transaction is prepared. + auto numReaped = stdx::erase_if(sri->childSessions, [&](auto&& it) { + ObservableSession osession(ul, sri, &it.second); + if (it.first.getTxnNumber() && *it.first.getTxnNumber() < *clientTxnNumberStarted) { + osession.markForReap(ObservableSession::ReapMode::kExclusive); + } + + bool willReap = osession._shouldBeReaped(); + if (willReap) { + eagerlyReapedSessions.push_back(std::move(it.first)); + } + return willReap; + }); + + LOGV2_DEBUG(6685200, + 4, + "Erased child sessions", + "releasedLsid"_attr = session->getSessionId(), + "clientTxnNumber"_attr = *clientTxnNumberStarted, + "childSessionsRemaining"_attr = sri->childSessions.size(), + "numReaped"_attr = numReaped); + } + + invariant(ul); + ul.unlock(); + + if (eagerlyReapedSessions.size() && _onEagerlyReapedSessionsFn) { + (*_onEagerlyReapedSessionsFn)(service, std::move(eagerlyReapedSessions)); + } +} + +Session* SessionCatalog::SessionRuntimeInfo::getSession(WithLock, const LogicalSessionId& lsid) { + if (isParentSessionId(lsid)) { + // We should have already compared the parent lsid when we found this SRI. + dassert(lsid == parentSession._sessionId); + return &parentSession; + } + + dassert(getParentSessionId(lsid) == parentSession._sessionId); + auto it = childSessions.find(lsid); + if (it == childSessions.end()) { + return nullptr; + } + return &it->second; +} + +SessionCatalog::KillToken ObservableSession::kill(ErrorCodes::Error reason) const { + const bool firstKiller = (0 == _sri->killsRequested); + ++_sri->killsRequested; + + if (firstKiller && hasCurrentOperation()) { + // Interrupt the current OperationContext if its running on the transaction session + // that is being killed or if we are killing the parent transaction session. + invariant(_clientLock.owns_lock()); + const auto checkedOutLsid = _sri->checkoutOpCtx->getLogicalSessionId(); + const auto lsidToKill = getSessionId(); + const bool isKillingParentSession = isParentSessionId(lsidToKill); + if (isKillingParentSession || (checkedOutLsid == lsidToKill)) { + const auto serviceContext = _sri->checkoutOpCtx->getServiceContext(); + serviceContext->killOperation(_clientLock, _sri->checkoutOpCtx, reason); + } + } + + return SessionCatalog::KillToken(getSessionId()); +} + +void ObservableSession::markForReap(ReapMode reapMode) { + if (isParentSessionId(getSessionId())) { + // By design, parent sessions are only safe to be reaped if all of their child sessions are. + invariant(reapMode == ReapMode::kNonExclusive); + } + _markedForReap = true; + _reapMode.emplace(reapMode); +} + +bool ObservableSession::_shouldBeReaped() const { + bool isCheckedOut = [&] { + if (_sri->checkoutOpCtx) { + return _sri->checkoutOpCtx->getLogicalSessionId() == getSessionId(); + } + return false; + }(); + return _markedForReap && !isCheckedOut && !get()->_numWaitingToCheckOut && !_killed(); +} + +bool ObservableSession::_killed() const { + return _sri->killsRequested > 0; +} + +OperationContextSession::OperationContextSession(OperationContext* opCtx) : _opCtx(opCtx) { + auto& checkedOutSession = operationSessionDecoration(opCtx); + if (checkedOutSession) { + // The only case where a session can be checked-out more than once is due to DBDirectClient + // reentrancy + invariant(opCtx->getClient()->isInDirectClient()); + return; + } + + checkOut(opCtx); +} + +OperationContextSession::OperationContextSession(OperationContext* opCtx, + SessionCatalog::KillToken killToken) + : _opCtx(opCtx) { + auto& checkedOutSession = operationSessionDecoration(opCtx); + + invariant(!checkedOutSession); + invariant(!opCtx->getLogicalSessionId()); // lsid is specified by killToken argument. + + const auto catalog = SessionCatalog::get(opCtx); + auto scopedSessionForKill = catalog->checkOutSessionForKill(opCtx, std::move(killToken)); + + // We acquire a Client lock here to guard the construction of this session so that references to + // this session are safe to use while the lock is held + stdx::lock_guard lk(*opCtx->getClient()); + checkedOutSession.emplace(std::move(scopedSessionForKill._scos)); +} + +OperationContextSession::~OperationContextSession() { + // Only release the checked out session at the end of the top-level request from the client, not + // at the end of a nested DBDirectClient call + if (_opCtx->getClient()->isInDirectClient()) { + return; + } + + auto& checkedOutSession = operationSessionDecoration(_opCtx); + if (!checkedOutSession) + return; + + checkIn(_opCtx, CheckInReason::kDone); +} + +Session* OperationContextSession::get(OperationContext* opCtx) { + auto& checkedOutSession = operationSessionDecoration(opCtx); + if (checkedOutSession) { + return checkedOutSession->get(); + } + + return nullptr; +} + +void OperationContextSession::checkIn(OperationContext* opCtx, CheckInReason reason) { + auto& checkedOutSession = operationSessionDecoration(opCtx); + invariant(checkedOutSession); + + if (reason == CheckInReason::kYield) { + // Don't allow yielding a session that was checked out for kill because it will "unkill" the + // session and the subsequent check out will not have priority, which can easily lead to + // bugs. If you need to run an operation with a session that may yield, kill the session, + // check it out for kill, release it, then check it out normally. + invariant(!checkedOutSession->wasCheckedOutForKill()); + } + + // Removing the checkedOutSession from the OperationContext must be done under the Client lock, + // but destruction of the checkedOutSession must not be, as it takes the SessionCatalog mutex, + // and other code may take the Client lock while holding that mutex. + stdx::unique_lock<Client> lk(*opCtx->getClient()); + SessionCatalog::ScopedCheckedOutSession sessionToReleaseOutOfLock( + std::move(*checkedOutSession)); + + // This destroys the moved-from ScopedCheckedOutSession, and must be done within the client lock + checkedOutSession = boost::none; + lk.unlock(); +} + +void OperationContextSession::checkOut(OperationContext* opCtx) { + auto& checkedOutSession = operationSessionDecoration(opCtx); + invariant(!checkedOutSession); + + const auto catalog = SessionCatalog::get(opCtx); + auto scopedCheckedOutSession = catalog->_checkOutSession(opCtx); + + // We acquire a Client lock here to guard the construction of this session so that references to + // this session are safe to use while the lock is held + stdx::lock_guard<Client> lk(*opCtx->getClient()); + checkedOutSession.emplace(std::move(scopedCheckedOutSession)); +} + +void OperationContextSession::observeNewTxnNumberStarted(OperationContext* opCtx, + const LogicalSessionId& lsid, + TxnNumber txnNumber) { + auto& checkedOutSession = operationSessionDecoration(opCtx); + invariant(checkedOutSession); + + LOGV2_DEBUG(6685201, + 4, + "Observing new retryable write number started on session", + "lsid"_attr = lsid, + "txnNumber"_attr = txnNumber); + + const auto& checkedOutLsid = (*checkedOutSession)->getSessionId(); + if (isParentSessionId(lsid)) { + // Observing a new transaction/retryable write on a parent session. + + // The operation must have checked out the parent session itself or a child session of the + // parent. This is safe because both share the same SessionRuntimeInfo. + dassert(lsid == checkedOutLsid || lsid == *getParentSessionId(checkedOutLsid)); + + checkedOutSession->observeNewClientTxnNumberStarted(txnNumber); + } else if (isInternalSessionForRetryableWrite(lsid)) { + // Observing a new internal transaction on a retryable session. + + // A transaction on a child session is always begun on an operation that checked it out + // directly. + dassert(lsid == checkedOutLsid); + + checkedOutSession->observeNewClientTxnNumberStarted(*lsid.getTxnNumber()); + } +} + +} // namespace mongo diff --git a/src/mongo/db/session/session_catalog.h b/src/mongo/db/session/session_catalog.h new file mode 100644 index 00000000000..bba338caf79 --- /dev/null +++ b/src/mongo/db/session/session_catalog.h @@ -0,0 +1,521 @@ +/** + * Copyright (C) 2018-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * 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 + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * 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 Server Side 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 <vector> + +#include "mongo/db/client.h" +#include "mongo/db/operation_context.h" +#include "mongo/db/session/logical_session_id.h" +#include "mongo/db/session/logical_session_id_helpers.h" +#include "mongo/db/session/session.h" +#include "mongo/db/session/session_killer.h" +#include "mongo/platform/mutex.h" +#include "mongo/stdx/condition_variable.h" +#include "mongo/stdx/unordered_map.h" +#include "mongo/util/concurrency/with_lock.h" +#include "mongo/util/hierarchical_acquisition.h" + +namespace mongo { + +class ObservableSession; + +/** + * Keeps track of the transaction runtime state for every active transaction session on this + * instance. + */ +class SessionCatalog { + SessionCatalog(const SessionCatalog&) = delete; + SessionCatalog& operator=(const SessionCatalog&) = delete; + + friend class ObservableSession; + friend class OperationContextSession; + +public: + using OnEagerlyReapedSessionsFn = + unique_function<void(ServiceContext*, std::vector<LogicalSessionId>)>; + + class ScopedCheckedOutSession; + class SessionToKill; + + struct KillToken { + KillToken(LogicalSessionId lsid) : lsidToKill(std::move(lsid)) {} + KillToken(KillToken&&) = default; + KillToken& operator=(KillToken&&) = default; + + LogicalSessionId lsidToKill; + }; + + SessionCatalog() = default; + ~SessionCatalog(); + + /** + * Retrieves the session transaction table associated with the service or operation context. + */ + static SessionCatalog* get(OperationContext* opCtx); + static SessionCatalog* get(ServiceContext* service); + + /** + * Resets the transaction table to an uninitialized state. + * Meant only for testing. + */ + void reset_forTest(); + + /** + * See the description of 'ObservableSession::kill' for more information on the session kill + * usage pattern. + */ + SessionToKill checkOutSessionForKill(OperationContext* opCtx, KillToken killToken); + + using ScanSessionsCallbackFn = std::function<void(ObservableSession&)>; + + /** + * Iterates through the SessionCatalog under the SessionCatalog mutex and applies 'workerFn' to + * each Session which matches the specified 'lsid' or 'matcher'. Does not support reaping. + * + * NOTE: Since this method runs with the session catalog mutex, the work done by 'workerFn' is + * not allowed to block, perform I/O or acquire any lock manager locks. + * Iterates through the SessionCatalog and applies 'workerFn' to each Session. This locks the + * SessionCatalog. + */ + enum class ScanSessionCreateSession { kYes, kNo }; + void scanSession(const LogicalSessionId& lsid, + const ScanSessionsCallbackFn& workerFn, + ScanSessionCreateSession createSession = ScanSessionCreateSession::kNo); + void scanSessions(const SessionKiller::Matcher& matcher, + const ScanSessionsCallbackFn& workerFn); + + /** + * Same as the above but only applies 'workerFn' to parent Sessions. + */ + void scanParentSessions(const ScanSessionsCallbackFn& workerFn); + + /** + * Same as the above but applies 'parentSessionWorkerFn' to the Session whose session id is + * equal to 'parentLsid' and then applies 'childSessionWorkerFn' to the Sessions whose parent + * session id is equal to 'parentLsid'. To be used with 'markForReap' for reaping sessions + * from the SessionCatalog. It enables transaction sessions that corresponds to the same + * logical session to be reaped atomically. Returns the session ids for the matching Sessions + * that were not reaped after the scan. + */ + LogicalSessionIdSet scanSessionsForReap(const LogicalSessionId& parentLsid, + const ScanSessionsCallbackFn& parentSessionWorkerFn, + const ScanSessionsCallbackFn& childSessionWorkerFn); + + /** + * Shortcut to invoke 'kill' on the specified session under the SessionCatalog mutex. Throws a + * NoSuchSession exception if the session doesn't exist. + */ + KillToken killSession(const LogicalSessionId& lsid); + + /** + * Returns the total number of entries currently cached on the session catalog. + */ + size_t size() const; + + /** + * Registers a callback to run when sessions are "eagerly" reaped from the catalog, ie without + * waiting for a logical session cache refresh. + */ + void setOnEagerlyReapedSessionsFn(OnEagerlyReapedSessionsFn fn) { + invariant(!_onEagerlyReapedSessionsFn); + _onEagerlyReapedSessionsFn = std::move(fn); + } + +private: + /** + * Tracks the runtime info for transaction sessions that corresponds to the same logical + * session. Designed such that only one transaction session can be checked out at any given + * time. + */ + struct SessionRuntimeInfo { + SessionRuntimeInfo(LogicalSessionId lsid) : parentSession(std::move(lsid)) { + // Can only create a SessionRuntimeInfo with a parent transaction session id. + invariant(isParentSessionId(lsid)); + } + + Session* getSession(WithLock, const LogicalSessionId& lsid); + + // Must only be accessed by the OperationContext which currently has this logical session + // checked out. + Session parentSession; + LogicalSessionIdMap<Session> childSessions; + + // Signaled when the state becomes available. Uses the transaction table's mutex to protect + // the state transitions. + stdx::condition_variable availableCondVar; + + // Pointer to the OperationContext for the operation running on this logical session, or + // nullptr if there is no operation currently running on the session. + OperationContext* checkoutOpCtx{nullptr}; + + // Last check-out time for this logical session. Updated every time any of the transaction + // sessions gets checked out. + Date_t lastCheckout{Date_t::now()}; + + // Counter indicating the number of times ObservableSession::kill has been called on this + // SessionRuntimeInfo, which have not yet had a corresponding call to + // checkOutSessionForKill. + int killsRequested{0}; + }; + using SessionRuntimeInfoMap = LogicalSessionIdMap<std::unique_ptr<SessionRuntimeInfo>>; + + /** + * Blocking method, which checks-out the session with the given 'lsid'. Called inside + * '_checkOutSession' and 'checkOutSessionForKill'. + */ + ScopedCheckedOutSession _checkOutSessionInner(OperationContext* opCtx, + const LogicalSessionId& lsid, + boost::optional<KillToken> killToken); + + /** + * Blocking method, which checks-out the session set on 'opCtx'. + */ + ScopedCheckedOutSession _checkOutSession(OperationContext* opCtx); + + /** + * Returns the session runtime info for 'lsid' from the '_sessions' map. The returned pointer + * is guaranteed to be linked on the map for as long as the mutex is held. + */ + SessionRuntimeInfo* _getSessionRuntimeInfo(WithLock lk, const LogicalSessionId& lsid); + + /** + * Creates or returns the session runtime info for 'lsid' from the '_sessions' map. The + * returned pointer is guaranteed to be linked on the map for as long as the mutex is held. + */ + SessionRuntimeInfo* _getOrCreateSessionRuntimeInfo(WithLock lk, const LogicalSessionId& lsid); + + /** + * Makes a session, previously checked out through 'checkoutSession', available again. Will free + * any retryable sessions with txnNumbers before clientTxnNumberStarted if it is set. + */ + void _releaseSession(SessionRuntimeInfo* sri, + Session* session, + boost::optional<KillToken> killToken, + boost::optional<TxnNumber> clientTxnNumberStarted); + + // Called when sessions are reaped from memory "eagerly" ie directly by the SessionCatalog + // without waiting for a logical session cache refresh. Note this is set at process startup + // before multi-threading is enabled, so no synchronization is necessary. + boost::optional<OnEagerlyReapedSessionsFn> _onEagerlyReapedSessionsFn; + + // Protects the state below + mutable Mutex _mutex = + MONGO_MAKE_LATCH(HierarchicalAcquisitionLevel(4), "SessionCatalog::_mutex"); + + // Owns the Session objects for all current Sessions. + SessionRuntimeInfoMap _sessions; +}; + +/** + * Scoped object representing a checked-out transaction session. This type is an implementation + * detail of the SessionCatalog. + */ +class SessionCatalog::ScopedCheckedOutSession { +public: + ScopedCheckedOutSession(SessionCatalog& catalog, + SessionCatalog::SessionRuntimeInfo* sri, + Session* session, + boost::optional<SessionCatalog::KillToken> killToken) + : _catalog(catalog), _sri(sri), _session(session), _killToken(std::move(killToken)) { + if (_killToken) { + invariant(session->getSessionId() == _killToken->lsidToKill); + } + } + + ScopedCheckedOutSession(ScopedCheckedOutSession&& other) + : _catalog(other._catalog), + _clientTxnNumberStarted(other._clientTxnNumberStarted), + _sri(other._sri), + _session(other._session), + _killToken(std::move(other._killToken)) { + other._sri = nullptr; + } + + ScopedCheckedOutSession& operator=(ScopedCheckedOutSession&&) = delete; + ScopedCheckedOutSession(const ScopedCheckedOutSession&) = delete; + ScopedCheckedOutSession& operator=(ScopedCheckedOutSession&) = delete; + + ~ScopedCheckedOutSession() { + if (_sri) { + _catalog._releaseSession( + _sri, _session, std::move(_killToken), _clientTxnNumberStarted); + } + } + + OperationContext* currentOperation_forTest() const { + return _sri->checkoutOpCtx; + } + + Session* get() const { + return _session; + } + + Session* operator->() const { + return get(); + } + + Session& operator*() const { + return *get(); + } + + bool wasCheckedOutForKill() const { + return bool(_killToken); + } + + void observeNewClientTxnNumberStarted(TxnNumber txnNumber) { + _clientTxnNumberStarted = txnNumber; + } + +private: + // The owning session catalog into which the session should be checked back + SessionCatalog& _catalog; + + // If this session began a retryable write or transaction while checked out, this is set to the + // "client txnNumber" of that transaction, which is the top-level txnNumber for a retryable + // write or transaction sent by a client or the txnNumber in the sessionId for a retryable + // child transaction. + boost::optional<TxnNumber> _clientTxnNumberStarted; + + SessionCatalog::SessionRuntimeInfo* _sri; + Session* _session; + boost::optional<SessionCatalog::KillToken> _killToken; +}; + +class OperationContextSession; + +/** + * RAII type returned by SessionCatalog::checkOutSessionForKill. + * + * After calling kill() on an ObservableSession, let that ObservableSession go out + * of scope and in a context outside of SessionCatalog::scanSessions, call checkOutSessionForKill + * to get an instance of this type. Then, while holding that instance, perform any cleanup + * you need to perform on a session as part of killing it. More details in the description of + * ObservableSession::kill, below. + */ +class SessionCatalog::SessionToKill { +public: + SessionToKill(ScopedCheckedOutSession&& scos) : _scos(std::move(scos)) {} + + Session* get() const { + return _scos.get(); + } + + const LogicalSessionId& getSessionId() const { + return get()->getSessionId(); + } + + OperationContext* currentOperation_forTest() const { + return _scos.currentOperation_forTest(); + } + +private: + friend OperationContextSession; + + ScopedCheckedOutSession _scos; +}; +using SessionToKill = SessionCatalog::SessionToKill; + +/** + * This type represents access to a transaction session inside of a scanSessions loop. + * If you have one of these, you're in a scanSessions callback context, and so + * have locked the whole catalog and, if the observed session is bound to an operation context, + * you hold that operation context's client's mutex, as well. + */ +class ObservableSession { +public: + ObservableSession(const ObservableSession&) = delete; + ObservableSession(ObservableSession&&) = delete; + ObservableSession& operator=(const ObservableSession&) = delete; + ObservableSession& operator=(ObservableSession&&) = delete; + + /** + * The session id for this transaction session. + */ + const LogicalSessionId& getSessionId() const { + return _session->_sessionId; + } + + /** + * Returns true if there is an operation currently running on the logical session that this + * transaction session corresponds to. + */ + bool hasCurrentOperation() const { + return _sri->checkoutOpCtx; + } + + /** + * Returns the last check-out time for the logical session that this transaction session + * corresponds to. Used for reaping purposes. + */ + Date_t getLastCheckout() const { + return _sri->lastCheckout; + } + + /** + * Increments the number of "killers" for the logical session that this transaction session + * corresponds to and returns a 'kill token' to to be passed later on to + * 'checkOutSessionForKill' method of the SessionCatalog in order to permit the caller to + * execute any kill cleanup tasks. This token is later used to decrement the number of + * "killers". + * + * Marking session as killed is an internal property only that will cause any further calls to + * 'checkOutSession' to block until 'checkOutSessionForKill' is called the same number of times + * as 'kill' was called and the returned scoped object destroyed. + * + * If the first killer finds the session checked-out, this method will also interrupt the + * operation context which has it checked-out. + */ + SessionCatalog::KillToken kill(ErrorCodes::Error reason = ErrorCodes::Interrupted) const; + + /** + * To be used with 'scanSessionsForReap' to indicate to the SessionCatalog that, from the user + * perspective, this transaction session is safe to be reaped. That is, the reaper has checked + * that the session has expired and all the decorations they are using are prepared to be + * destroyed. There are two reap modes: + * - kExclusive indicates that the session is safe to be reaped independently of the other + * sessions matched by 'scanSessionsForReap'. + * - kNonExclusive indicates that the session is only safe to reaped if all the other sessions + * are also safe to be reaped. + * + * Calling this method does not guarantee that the session will in fact be reaped. The + * SessionCatalog performs additional checks to protect sessions that are still in use from + * being reaped. However, reaping will still obey the specified reap mode. See the comment for + * '_shouldBeReaped' for more info. + */ + enum class ReapMode { kExclusive, kNonExclusive }; + void markForReap(ReapMode reapMode); + + /** + * Returns a pointer to the Session itself. + */ + Session* get() const { + return _session; + } + +private: + friend class SessionCatalog; + + static stdx::unique_lock<Client> _lockClientForSession( + WithLock, SessionCatalog::SessionRuntimeInfo* sri) { + if (const auto opCtx = sri->checkoutOpCtx) { + return stdx::unique_lock<Client>{*opCtx->getClient()}; + } + return {}; + } + + ObservableSession(WithLock wl, SessionCatalog::SessionRuntimeInfo* sri, Session* session) + : _sri(sri), _session(session), _clientLock(_lockClientForSession(std::move(wl), _sri)) {} + + /** + * Returns whether 'kill' has been called on this session. + */ + bool _killed() const; + + /** + * Returns true if this Session can be checked out. + */ + bool _isAvailableForCheckOut(bool forKill) const { + return !hasCurrentOperation() && (forKill || !_killed()); + } + + /** + * Returns true if this transaction session should be be reaped from the SessionCatalog. + * That is, the session has been marked for reap and both of the following are true: + * - It is not checked out by any thread, and there are no threads waiting for it to be + * checked out. + * - It is not marked for kill (i.e. expected to be checked out for kill). + */ + bool _shouldBeReaped() const; + + SessionCatalog::SessionRuntimeInfo* _sri; + Session* _session; + stdx::unique_lock<Client> _clientLock; + + bool _markedForReap{false}; + boost::optional<ReapMode> _reapMode; +}; + +/** + * 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 OperationContextSession { + OperationContextSession(const OperationContextSession&) = delete; + OperationContextSession& operator=(const OperationContextSession&) = delete; + +public: + /** + * Acquires the session with id opCtx->getLogicalSessionId(). Because a session can only be + * checked out by one user at a time, construction of OperationContextSession can block waiting + * for the desired session to be checked in by another user. + */ + OperationContextSession(OperationContext* opCtx); + + /** + * Same as constructor above, but takes the session id from the killToken and uses + * checkoutSessionForKill instead, placing the checked-out session on the operation context. + * Must not be called if the operation context contains a session. + */ + OperationContextSession(OperationContext* opCtx, SessionCatalog::KillToken killToken); + ~OperationContextSession(); + + /** + * Returns the session currently checked out by "opCtx", or nullptr if the opCtx has no + * checked out session. + */ + static Session* get(OperationContext* opCtx); + + /** + * These methods take an operation context with a checked-out session and allow it to be + * temporarily or permanently checked back in, in order to allow other operations to use it. + * + * Check-in may only be called if the session has actually been checked out previously and + * similarly check-out may only be called if the session is not checked out already. + */ + enum class CheckInReason { kDone, kYield }; + static void checkIn(OperationContext* opCtx, CheckInReason reason); + static void checkOut(OperationContext* opCtx); + + /** + * Notifies the session catalog when a new transaction/retryable write is begun on the operation + * context's checked out session. + */ + static void observeNewTxnNumberStarted(OperationContext* opCtx, + const LogicalSessionId& lsid, + TxnNumber txnNumber); + +private: + OperationContext* const _opCtx; +}; + +} // namespace mongo diff --git a/src/mongo/db/session/session_catalog_mongod.cpp b/src/mongo/db/session/session_catalog_mongod.cpp new file mode 100644 index 00000000000..1d5bc3df966 --- /dev/null +++ b/src/mongo/db/session/session_catalog_mongod.cpp @@ -0,0 +1,726 @@ +/** + * Copyright (C) 2018-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * 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 + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * 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 Server Side 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/session/session_catalog_mongod.h" + +#include "mongo/bson/bsonmisc.h" +#include "mongo/db/catalog_raii.h" +#include "mongo/db/client.h" +#include "mongo/db/create_indexes_gen.h" +#include "mongo/db/dbdirectclient.h" +#include "mongo/db/index_builds_coordinator.h" +#include "mongo/db/internal_transactions_feature_flag_gen.h" +#include "mongo/db/namespace_string.h" +#include "mongo/db/operation_context.h" +#include "mongo/db/ops/write_ops.h" +#include "mongo/db/repl/repl_server_parameters_gen.h" +#include "mongo/db/repl/storage_interface_impl.h" +#include "mongo/db/service_context.h" +#include "mongo/db/session/session_txn_record_gen.h" +#include "mongo/db/session/sessions_collection.h" +#include "mongo/db/transaction/transaction_participant.h" +#include "mongo/logv2/log.h" +#include "mongo/rpc/get_status_from_command_result.h" +#include "mongo/s/transaction_router.h" +#include "mongo/util/concurrency/thread_pool.h" +#include "mongo/util/scopeguard.h" + +#define MONGO_LOGV2_DEFAULT_COMPONENT ::mongo::logv2::LogComponent::kTransaction + + +namespace mongo { +namespace { + +struct SessionTasksExecutor { + SessionTasksExecutor() + : threadPool([] { + ThreadPool::Options options; + options.threadNamePrefix = "MongoDSessionCatalog"; + options.minThreads = 0; + options.maxThreads = 1; + return options; + }()) {} + + ThreadPool threadPool; +}; + +const auto sessionTasksExecutor = ServiceContext::declareDecoration<SessionTasksExecutor>(); +const ServiceContext::ConstructorActionRegisterer sessionTasksExecutorRegisterer{ + "SessionCatalogD", + [](ServiceContext* service) { sessionTasksExecutor(service).threadPool.startup(); }, + [](ServiceContext* service) { + auto& pool = sessionTasksExecutor(service).threadPool; + pool.shutdown(); + pool.join(); + }}; + +auto getThreadPool(OperationContext* opCtx) { + return &sessionTasksExecutor(opCtx->getServiceContext()).threadPool; +} + +/** + * Non-blocking call, which schedules asynchronously the work to finish cleaning up the specified + * set of kill tokens. + */ +void killSessionTokens(OperationContext* opCtx, + std::vector<SessionCatalog::KillToken> sessionKillTokens) { + if (sessionKillTokens.empty()) + return; + + getThreadPool(opCtx)->schedule( + [service = opCtx->getServiceContext(), + sessionKillTokens = std::move(sessionKillTokens)](auto status) mutable { + invariant(status); + + ThreadClient tc("Kill-Sessions", service); + auto uniqueOpCtx = tc->makeOperationContext(); + const auto opCtx = uniqueOpCtx.get(); + const auto catalog = SessionCatalog::get(opCtx); + + for (auto& sessionKillToken : sessionKillTokens) { + auto session = catalog->checkOutSessionForKill(opCtx, std::move(sessionKillToken)); + auto participant = TransactionParticipant::get(session); + participant.invalidate(opCtx); + } + }); +} + +void disallowDirectWritesUnderSession(OperationContext* opCtx) { + const auto replCoord = repl::ReplicationCoordinator::get(opCtx); + bool isReplSet = replCoord->getReplicationMode() == repl::ReplicationCoordinator::modeReplSet; + if (isReplSet) { + uassert(40528, + str::stream() << "Direct writes against " + << NamespaceString::kSessionTransactionsTableNamespace + << " cannot be performed using a transaction or on a session.", + !opCtx->getLogicalSessionId()); + } +} + +/** + * Removes the transaction sessions that are expired and not in use from the in-memory catalog + * (i.e. SessionCatalog). Returns the session ids for the expired transaction sessions that were + * not removed because they were in use. + */ +LogicalSessionIdSet removeExpiredTransactionSessionsNotInUseFromMemory( + OperationContext* opCtx, SessionsCollection& sessionsCollection, Date_t possiblyExpired) { + const auto catalog = SessionCatalog::get(opCtx); + + // Find the possibly expired logical session ids in the in-memory catalog. + LogicalSessionIdSet possiblyExpiredLogicalSessionIds; + // Skip child transaction sessions since they correspond to the same logical session as their + // parent transaction session so they have the same last check-out time as the parent's. + catalog->scanParentSessions([&](const ObservableSession& session) { + const auto sessionId = session.getSessionId(); + invariant(isParentSessionId(sessionId)); + if (session.getLastCheckout() < possiblyExpired) { + possiblyExpiredLogicalSessionIds.insert(sessionId); + } + }); + // From the possibly expired logical session ids, find the ones that have been removed from + // from the config.system.sessions collection. + LogicalSessionIdSet expiredLogicalSessionIds = + sessionsCollection.findRemovedSessions(opCtx, possiblyExpiredLogicalSessionIds); + + // For each removed logical session id, removes all of its transaction session ids that are no + // longer in use from the in-memory catalog. + LogicalSessionIdSet expiredTransactionSessionIdsStillInUse; + for (const auto& expiredLogicalSessionId : expiredLogicalSessionIds) { + invariant(isParentSessionId(expiredLogicalSessionId)); + + // Scan all the transaction sessions for this logical session at once so reaping can be done + // atomically. + TxnNumber parentSessionActiveTxnNumber; + const auto transactionSessionIdsNotReaped = catalog->scanSessionsForReap( + expiredLogicalSessionId, + [&](ObservableSession& parentSession) { + const auto transactionSessionId = parentSession.getSessionId(); + const auto txnParticipant = TransactionParticipant::get(parentSession); + const auto txnRouter = TransactionRouter::get(parentSession); + + parentSessionActiveTxnNumber = + txnParticipant.getActiveTxnNumberAndRetryCounter().getTxnNumber(); + if (txnParticipant.canBeReaped() && txnRouter.canBeReaped()) { + LOGV2_DEBUG(6753702, + 5, + "Marking parent transaction session for reap", + "lsid"_attr = transactionSessionId); + // This is an external session so it can be reaped if and only if all of its + // internal sessions can be reaped. + parentSession.markForReap(ObservableSession::ReapMode::kNonExclusive); + } + }, + [&](ObservableSession& childSession) { + const auto transactionSessionId = childSession.getSessionId(); + const auto txnParticipant = TransactionParticipant::get(childSession); + const auto txnRouter = TransactionRouter::get(childSession); + + if (txnParticipant.canBeReaped() && txnRouter.canBeReaped()) { + if (isInternalSessionForNonRetryableWrite(transactionSessionId)) { + LOGV2_DEBUG(6753703, + 5, + "Marking child transaction session for reap", + "lsid"_attr = transactionSessionId); + // This is an internal session for a non-retryable write so it can be reaped + // independently of the external session that write ran in. + childSession.markForReap(ObservableSession::ReapMode::kExclusive); + } else if (isInternalSessionForRetryableWrite(transactionSessionId)) { + LOGV2_DEBUG(6753704, + 5, + "Marking child transaction session for reap", + "lsid"_attr = transactionSessionId); + // This is an internal session for a retryable write so it must be reaped + // atomically with the external session and internal sessions for that + // retryable write, unless the write is no longer active (i.e. there is + // already a retryable write or transaction with a higher txnNumber). + childSession.markForReap(*transactionSessionId.getTxnNumber() < + parentSessionActiveTxnNumber + ? ObservableSession::ReapMode::kExclusive + : ObservableSession::ReapMode::kNonExclusive); + } else { + MONGO_UNREACHABLE; + } + } + }); + expiredTransactionSessionIdsStillInUse.insert(transactionSessionIdsNotReaped.begin(), + transactionSessionIdsNotReaped.end()); + } + + LOGV2_DEBUG(6753705, + 5, + "Expired sessions not reaped from the SessionCatalog", + "lsids"_attr = expiredTransactionSessionIdsStillInUse); + + return expiredTransactionSessionIdsStillInUse; +} + +const auto kIdProjection = BSON(SessionTxnRecord::kSessionIdFieldName << 1); +const auto kSortById = BSON(SessionTxnRecord::kSessionIdFieldName << 1); +const auto kLastWriteDateFieldName = SessionTxnRecord::kLastWriteDateFieldName; + +template <typename SessionContainer> +int removeSessionsTransactionRecordsFromDisk(OperationContext* opCtx, + const SessionContainer& transactionSessionIdsToReap) { + if (transactionSessionIdsToReap.empty()) { + return 0; + } + + // Remove the config.image_collection entries for the expired transaction session ids. We first + // delete any images belonging to sessions about to be reaped, followed by the sessions. This + // way if there's a failure, we'll only be left with sessions that have a dangling reference + // to an image. Session reaping will rediscover the sessions to delete and try again. + // + // We opt for this rather than performing the two sets of deletes in a single transaction simply + // to reduce code complexity. + DBDirectClient client(opCtx); + write_ops::checkWriteErrors(client.remove([&] { + write_ops::DeleteCommandRequest imageDeleteOp(NamespaceString::kConfigImagesNamespace); + imageDeleteOp.setWriteCommandRequestBase([] { + write_ops::WriteCommandRequestBase base; + base.setOrdered(false); + return base; + }()); + imageDeleteOp.setDeletes([&] { + std::vector<write_ops::DeleteOpEntry> entries; + for (const auto& transactionSessionId : transactionSessionIdsToReap) { + entries.emplace_back( + BSON(LogicalSessionRecord::kIdFieldName << transactionSessionId.toBSON()), + false /* multi = false */); + } + return entries; + }()); + return imageDeleteOp; + }())); + + // Remove the config.transaction entries for the expired transaction session ids. + auto sessionDeleteReply = write_ops::checkWriteErrors(client.remove([&] { + write_ops::DeleteCommandRequest sessionDeleteOp( + NamespaceString::kSessionTransactionsTableNamespace); + sessionDeleteOp.setWriteCommandRequestBase([] { + write_ops::WriteCommandRequestBase base; + base.setOrdered(false); + return base; + }()); + sessionDeleteOp.setDeletes([&] { + std::vector<write_ops::DeleteOpEntry> entries; + for (const auto& transactionSessionId : transactionSessionIdsToReap) { + entries.emplace_back( + BSON(LogicalSessionRecord::kIdFieldName << transactionSessionId.toBSON()), + false /* multi = false */); + } + return entries; + }()); + return sessionDeleteOp; + }())); + + return sessionDeleteReply.getN(); +} + +/** + * Removes the config.transactions and the config.image_collection entries for the transaction + * sessions in 'expiredTransactionSessionIdsNotInUse' whose logical sessions have expired. Returns + * the number of transaction sessions whose entries were removed. + */ +int removeSessionsTransactionRecordsIfExpired( + OperationContext* opCtx, + SessionsCollection& sessionsCollection, + const LogicalSessionIdSet& expiredTransactionSessionIdsNotInUse) { + if (expiredTransactionSessionIdsNotInUse.empty()) { + return 0; + } + + // From the expired transaction session ids that are no longer in use, find the ones whose + // logical sessions have been removed from from the config.system.sessions collection. + LogicalSessionIdSet transactionSessionIdsToReap; + { + LogicalSessionIdSet possiblyExpiredLogicalSessionIds; + for (const auto& transactionSessionId : expiredTransactionSessionIdsNotInUse) { + const auto logicalSessionId = isParentSessionId(transactionSessionId) + ? transactionSessionId + : *getParentSessionId(transactionSessionId); + possiblyExpiredLogicalSessionIds.insert(std::move(logicalSessionId)); + } + auto expiredLogicalSessionIds = + sessionsCollection.findRemovedSessions(opCtx, possiblyExpiredLogicalSessionIds); + + for (const auto& transactionSessionId : expiredTransactionSessionIdsNotInUse) { + const auto logicalSessionId = isParentSessionId(transactionSessionId) + ? transactionSessionId + : *getParentSessionId(transactionSessionId); + if (expiredLogicalSessionIds.find(logicalSessionId) != expiredLogicalSessionIds.end()) { + transactionSessionIdsToReap.insert(transactionSessionId); + } + } + } + + return removeSessionsTransactionRecordsFromDisk(opCtx, transactionSessionIdsToReap); +} + +/** + * Removes the transaction sessions that are expired and not in use from the on-disk catalog (i.e. + * the config.transactions collection and the config.image_collection collection). Returns the + * number of transaction sessions whose entries were removed. + */ +int removeExpiredTransactionSessionsFromDisk( + OperationContext* opCtx, + SessionsCollection& sessionsCollection, + Date_t possiblyExpired, + const LogicalSessionIdSet& expiredTransactionSessionIdsStillInUse) { + // Scan for records older than the minimum lifetime and uses a sort to walk the '_id' index. + DBDirectClient client(opCtx); + FindCommandRequest findRequest{NamespaceString::kSessionTransactionsTableNamespace}; + findRequest.setFilter(BSON(kLastWriteDateFieldName << LT << possiblyExpired)); + findRequest.setSort(kSortById); + findRequest.setProjection(kIdProjection); + auto cursor = client.find(std::move(findRequest)); + + LogicalSessionIdSet expiredTransactionSessionIdsNotInUse; + int numReaped = 0; + while (cursor->more()) { + auto transactionSession = SessionsCollectionFetchResultIndividualResult::parse( + IDLParserContext{"TransactionSession"}, cursor->next()); + const auto transactionSessionId = transactionSession.get_id(); + + if (expiredTransactionSessionIdsStillInUse.find(transactionSessionId) != + expiredTransactionSessionIdsStillInUse.end()) { + continue; + } + + expiredTransactionSessionIdsNotInUse.insert(transactionSessionId); + if (expiredTransactionSessionIdsNotInUse.size() > + MongoDSessionCatalog::kMaxSessionDeletionBatchSize) { + numReaped += removeSessionsTransactionRecordsIfExpired( + opCtx, sessionsCollection, expiredTransactionSessionIdsNotInUse); + expiredTransactionSessionIdsNotInUse.clear(); + } + } + numReaped += removeSessionsTransactionRecordsIfExpired( + opCtx, sessionsCollection, expiredTransactionSessionIdsNotInUse); + + return numReaped; +} + +void createTransactionTable(OperationContext* opCtx) { + CollectionOptions options; + auto storageInterface = repl::StorageInterface::get(opCtx); + auto createCollectionStatus = storageInterface->createCollection( + opCtx, NamespaceString::kSessionTransactionsTableNamespace, options); + + if (createCollectionStatus == ErrorCodes::NamespaceExists) { + bool collectionIsEmpty = false; + { + AutoGetCollection autoColl( + opCtx, NamespaceString::kSessionTransactionsTableNamespace, LockMode::MODE_IS); + invariant(autoColl); + + if (autoColl->getIndexCatalog()->findIndexByName( + opCtx, MongoDSessionCatalog::kConfigTxnsPartialIndexName)) { + // Index already exists, so there's nothing to do. + return; + } + + collectionIsEmpty = autoColl->isEmpty(opCtx); + } + + if (!collectionIsEmpty) { + // Unless explicitly enabled, don't create the index to avoid delaying step up. + if (feature_flags::gFeatureFlagAlwaysCreateConfigTransactionsPartialIndexOnStepUp + .isEnabledAndIgnoreFCV()) { + AutoGetCollection autoColl( + opCtx, NamespaceString::kSessionTransactionsTableNamespace, LockMode::MODE_X); + IndexBuildsCoordinator::get(opCtx)->createIndex( + opCtx, + autoColl->uuid(), + MongoDSessionCatalog::getConfigTxnPartialIndexSpec(), + IndexBuildsManager::IndexConstraints::kEnforce, + false /* fromMigration */); + } + + return; + } + + // The index does not exist and the collection is empty, so fall through to create it on the + // empty collection. This can happen after a failover because the collection and index + // creation are recorded as separate oplog entries. + } else { + uassertStatusOKWithContext(createCollectionStatus, + str::stream() + << "Failed to create the " + << NamespaceString::kSessionTransactionsTableNamespace.ns() + << " collection"); + } + + auto indexSpec = MongoDSessionCatalog::getConfigTxnPartialIndexSpec(); + + const auto createIndexStatus = storageInterface->createIndexesOnEmptyCollection( + opCtx, NamespaceString::kSessionTransactionsTableNamespace, {indexSpec}); + uassertStatusOKWithContext( + createIndexStatus, + str::stream() << "Failed to create partial index for the " + << NamespaceString::kSessionTransactionsTableNamespace.ns() << " collection"); +} + +void createRetryableFindAndModifyTable(OperationContext* opCtx) { + auto serviceCtx = opCtx->getServiceContext(); + CollectionOptions options; + auto status = repl::StorageInterface::get(serviceCtx) + ->createCollection(opCtx, NamespaceString::kConfigImagesNamespace, options); + if (status == ErrorCodes::NamespaceExists) { + return; + } + + uassertStatusOKWithContext(status, + str::stream() << "Failed to create the " + << NamespaceString::kConfigImagesNamespace.ns() + << " collection"); +} + + +void abortInProgressTransactions(OperationContext* opCtx) { + DBDirectClient client(opCtx); + FindCommandRequest findRequest{NamespaceString::kSessionTransactionsTableNamespace}; + findRequest.setFilter(BSON(SessionTxnRecord::kStateFieldName + << DurableTxnState_serializer(DurableTxnStateEnum::kInProgress))); + auto cursor = client.find(std::move(findRequest)); + + if (cursor->more()) { + LOGV2_DEBUG(21977, 3, "Aborting in-progress transactions on stepup."); + } + while (cursor->more()) { + auto txnRecord = SessionTxnRecord::parse(IDLParserContext("abort-in-progress-transactions"), + cursor->next()); + opCtx->setLogicalSessionId(txnRecord.getSessionId()); + opCtx->setTxnNumber(txnRecord.getTxnNum()); + opCtx->setInMultiDocumentTransaction(); + MongoDOperationContextSessionWithoutRefresh ocs(opCtx); + auto txnParticipant = TransactionParticipant::get(opCtx); + LOGV2_DEBUG(21978, + 3, + "Aborting transaction sessionId: {sessionId} txnNumber {txnNumber}", + "Aborting transaction", + "sessionId"_attr = txnRecord.getSessionId().toBSON(), + "txnNumber"_attr = txnRecord.getTxnNum()); + txnParticipant.abortTransaction(opCtx); + opCtx->resetMultiDocumentTransactionState(); + } +} +} // namespace + +const std::string MongoDSessionCatalog::kConfigTxnsPartialIndexName = "parent_lsid"; + +BSONObj MongoDSessionCatalog::getConfigTxnPartialIndexSpec() { + NewIndexSpec index; + index.setV(int(IndexDescriptor::kLatestIndexVersion)); + index.setKey(BSON( + SessionTxnRecord::kParentSessionIdFieldName + << 1 + << (SessionTxnRecord::kSessionIdFieldName + "." + LogicalSessionId::kTxnNumberFieldName) + << 1 << SessionTxnRecord::kSessionIdFieldName << 1)); + index.setName(MongoDSessionCatalog::kConfigTxnsPartialIndexName); + index.setPartialFilterExpression(BSON("parentLsid" << BSON("$exists" << true))); + return index.toBSON(); +} + +void MongoDSessionCatalog::onStepUp(OperationContext* opCtx) { + // Invalidate sessions that could have a retryable write on it, so that we can refresh from disk + // in case the in-memory state was out of sync. + const auto catalog = SessionCatalog::get(opCtx); + + std::vector<SessionCatalog::KillToken> sessionKillTokens; + + // Scan all sessions and reacquire locks for prepared transactions. + // There may be sessions that are checked out during this scan, but none of them + // can be prepared transactions, since only oplog application can make transactions + // prepared on secondaries and oplog application has been stopped at this moment. + std::vector<OperationSessionInfo> sessionsToReacquireLocks; + + SessionKiller::Matcher matcher( + KillAllSessionsByPatternSet{makeKillAllSessionsByPattern(opCtx)}); + catalog->scanSessions(matcher, [&](const ObservableSession& session) { + const auto txnParticipant = TransactionParticipant::get(session); + if (!txnParticipant.transactionIsOpen()) { + sessionKillTokens.emplace_back(session.kill()); + } + + if (txnParticipant.transactionIsPrepared()) { + const auto txnNumberAndRetryCounter = + txnParticipant.getActiveTxnNumberAndRetryCounter(); + + OperationSessionInfo sessionInfo; + sessionInfo.setSessionId(session.getSessionId()); + sessionInfo.setTxnNumber(txnNumberAndRetryCounter.getTxnNumber()); + sessionInfo.setTxnRetryCounter(txnNumberAndRetryCounter.getTxnRetryCounter()); + sessionsToReacquireLocks.emplace_back(sessionInfo); + } + }); + killSessionTokens(opCtx, std::move(sessionKillTokens)); + + { + // Create a new opCtx because we need an empty locker to refresh the locks. + auto newClient = opCtx->getServiceContext()->makeClient("restore-prepared-txn"); + AlternativeClientRegion acr(newClient); + for (const auto& sessionInfo : sessionsToReacquireLocks) { + auto newOpCtx = cc().makeOperationContext(); + newOpCtx->setLogicalSessionId(*sessionInfo.getSessionId()); + newOpCtx->setTxnNumber(*sessionInfo.getTxnNumber()); + newOpCtx->setTxnRetryCounter(*sessionInfo.getTxnRetryCounter()); + newOpCtx->setInMultiDocumentTransaction(); + + // Use MongoDOperationContextSessionWithoutRefresh to check out the session because: + // - The in-memory state for this session has been kept in sync with the on-disk state + // by secondary oplog application for prepared transactions so no refresh will be + // done anyway. + // - The in-memory state for any external and/or internal sessions associated with this + // session may be out-of-date with the on-disk state but no refresh is necessary since + // the transaction is already in the prepared state so it no longer needs to go + // through conflict and write history check. In addition, a refresh of any session is + // expected to cause a deadlock since this 'newOpCtx' will need to acquire the global + // lock in the IS mode prior to reading the config.transactions collection but it + // cannot do that while the RSTL lock is being held by 'opCtx'. + MongoDOperationContextSessionWithoutRefresh ocs(newOpCtx.get()); + auto txnParticipant = TransactionParticipant::get(newOpCtx.get()); + LOGV2_DEBUG(21979, + 3, + "Restoring locks of prepared transaction. SessionId: {sessionId} " + "TxnNumberAndRetryCounter: {txnNumberAndRetryCounter}", + "Restoring locks of prepared transaction", + "sessionId"_attr = sessionInfo.getSessionId()->getId(), + "txnNumberAndRetryCounter"_attr = + txnParticipant.getActiveTxnNumberAndRetryCounter()); + txnParticipant.refreshLocksForPreparedTransaction(newOpCtx.get(), false); + } + } + + abortInProgressTransactions(opCtx); + + createTransactionTable(opCtx); + if (repl::feature_flags::gFeatureFlagRetryableFindAndModify.isEnabledAndIgnoreFCV()) { + createRetryableFindAndModifyTable(opCtx); + } +} + +boost::optional<UUID> MongoDSessionCatalog::getTransactionTableUUID(OperationContext* opCtx) { + AutoGetCollection coll(opCtx, NamespaceString::kSessionTransactionsTableNamespace, MODE_IS); + + if (!coll) { + return boost::none; + } + + return coll->uuid(); +} + +void MongoDSessionCatalog::observeDirectWriteToConfigTransactions(OperationContext* opCtx, + BSONObj singleSessionDoc) { + disallowDirectWritesUnderSession(opCtx); + + class KillSessionTokenOnCommit : public RecoveryUnit::Change { + public: + KillSessionTokenOnCommit(OperationContext* opCtx, + SessionCatalog::KillToken sessionKillToken) + : _opCtx(opCtx), _sessionKillToken(std::move(sessionKillToken)) {} + + void commit(boost::optional<Timestamp>) override { + rollback(); + } + + void rollback() override { + std::vector<SessionCatalog::KillToken> sessionKillTokenVec; + sessionKillTokenVec.emplace_back(std::move(_sessionKillToken)); + killSessionTokens(_opCtx, std::move(sessionKillTokenVec)); + } + + private: + OperationContext* _opCtx; + SessionCatalog::KillToken _sessionKillToken; + }; + + const auto catalog = SessionCatalog::get(opCtx); + + const auto lsid = + LogicalSessionId::parse(IDLParserContext("lsid"), singleSessionDoc["_id"].Obj()); + catalog->scanSession(lsid, [&](const ObservableSession& session) { + const auto participant = TransactionParticipant::get(session); + uassert(ErrorCodes::PreparedTransactionInProgress, + str::stream() << "Cannot modify the entry for session " + << session.getSessionId().getId() + << " because it is in the prepared state", + !participant.transactionIsPrepared()); + + opCtx->recoveryUnit()->registerChange( + std::make_unique<KillSessionTokenOnCommit>(opCtx, session.kill())); + }); +} + +void MongoDSessionCatalog::invalidateAllSessions(OperationContext* opCtx) { + disallowDirectWritesUnderSession(opCtx); + + const auto catalog = SessionCatalog::get(opCtx); + + std::vector<SessionCatalog::KillToken> sessionKillTokens; + + SessionKiller::Matcher matcher( + KillAllSessionsByPatternSet{makeKillAllSessionsByPattern(opCtx)}); + catalog->scanSessions(matcher, [&sessionKillTokens](const ObservableSession& session) { + sessionKillTokens.emplace_back(session.kill()); + }); + + killSessionTokens(opCtx, std::move(sessionKillTokens)); +} + +int MongoDSessionCatalog::reapSessionsOlderThan(OperationContext* opCtx, + SessionsCollection& sessionsCollection, + Date_t possiblyExpired) { + const auto expiredTransactionSessionIdsStillInUse = + removeExpiredTransactionSessionsNotInUseFromMemory( + opCtx, sessionsCollection, possiblyExpired); + + // The "unsafe" check for primary below is a best-effort attempt to ensure that the on-disk + // state reaping code doesn't run if the node is secondary and cause log spam. It is a work + // around the fact that the logical sessions cache is not registered to listen for replication + // state changes. + const auto replCoord = repl::ReplicationCoordinator::get(opCtx); + if (!replCoord->canAcceptWritesForDatabase_UNSAFE(opCtx, NamespaceString::kConfigDb)) + return 0; + + return removeExpiredTransactionSessionsFromDisk( + opCtx, sessionsCollection, possiblyExpired, expiredTransactionSessionIdsStillInUse); +} + +int MongoDSessionCatalog::removeSessionsTransactionRecords( + OperationContext* opCtx, const std::vector<LogicalSessionId>& transactionSessionIdsToRemove) { + std::vector<LogicalSessionId> nextLsidBatch; + int numReaped = 0; + for (const auto& transactionSessionIdToRemove : transactionSessionIdsToRemove) { + nextLsidBatch.push_back(transactionSessionIdToRemove); + if (nextLsidBatch.size() > MongoDSessionCatalog::kMaxSessionDeletionBatchSize) { + numReaped += removeSessionsTransactionRecordsFromDisk(opCtx, nextLsidBatch); + nextLsidBatch.clear(); + } + } + numReaped += removeSessionsTransactionRecordsFromDisk(opCtx, nextLsidBatch); + + return numReaped; +} + +MongoDOperationContextSession::MongoDOperationContextSession(OperationContext* opCtx) + : _operationContextSession(opCtx) { + invariant(!opCtx->getClient()->isInDirectClient()); + + auto txnParticipant = TransactionParticipant::get(opCtx); + txnParticipant.refreshFromStorageIfNeeded(opCtx); +} + +MongoDOperationContextSession::~MongoDOperationContextSession() = default; + +void MongoDOperationContextSession::checkIn(OperationContext* opCtx, + OperationContextSession::CheckInReason reason) { + OperationContextSession::checkIn(opCtx, reason); +} + +void MongoDOperationContextSession::checkOut(OperationContext* opCtx) { + OperationContextSession::checkOut(opCtx); + auto txnParticipant = TransactionParticipant::get(opCtx); + txnParticipant.refreshFromStorageIfNeeded(opCtx); +} + +MongoDOperationContextSessionWithoutRefresh::MongoDOperationContextSessionWithoutRefresh( + OperationContext* opCtx) + : _operationContextSession(opCtx), _opCtx(opCtx) { + invariant(!opCtx->getClient()->isInDirectClient()); + const auto clientTxnNumber = *opCtx->getTxnNumber(); + const auto clientTxnRetryCounter = *opCtx->getTxnRetryCounter(); + + auto txnParticipant = TransactionParticipant::get(opCtx); + txnParticipant.beginOrContinueTransactionUnconditionally( + opCtx, {clientTxnNumber, clientTxnRetryCounter}); +} + +MongoDOperationContextSessionWithoutRefresh::~MongoDOperationContextSessionWithoutRefresh() { + const auto txnParticipant = TransactionParticipant::get(_opCtx); + // A session on secondaries should never be checked back in with a TransactionParticipant that + // isn't prepared, aborted, or committed. + invariant(!txnParticipant.transactionIsInProgress()); +} + +MongoDOperationContextSessionWithoutOplogRead::MongoDOperationContextSessionWithoutOplogRead( + OperationContext* opCtx) + : _operationContextSession(opCtx), _opCtx(opCtx) { + invariant(!opCtx->getClient()->isInDirectClient()); + + auto txnParticipant = TransactionParticipant::get(opCtx); + txnParticipant.refreshFromStorageIfNeededNoOplogEntryFetch(opCtx); +} + +MongoDOperationContextSessionWithoutOplogRead::~MongoDOperationContextSessionWithoutOplogRead() = + default; + +} // namespace mongo diff --git a/src/mongo/db/session/session_catalog_mongod.h b/src/mongo/db/session/session_catalog_mongod.h new file mode 100644 index 00000000000..a2de7228bec --- /dev/null +++ b/src/mongo/db/session/session_catalog_mongod.h @@ -0,0 +1,159 @@ +/** + * Copyright (C) 2018-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * 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 + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * 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 Server Side 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 "mongo/db/session/session_catalog.h" + +namespace mongo { + +class SessionsCollection; + +class MongoDSessionCatalog { +public: + static const std::string kConfigTxnsPartialIndexName; + + // The max batch size is chosen so that a single batch won't exceed the 16MB BSON object size + // limit. + static const int kMaxSessionDeletionBatchSize = 10'000; + + /** + * Returns the specification for the partial index on config.transactions used to support + * retryable transactions. + */ + static BSONObj getConfigTxnPartialIndexSpec(); + + /** + * Invoked when the node enters the primary state. Ensures that the transactions collection is + * created. Throws on severe exceptions due to which it is not safe to continue the step-up + * process. + */ + static void onStepUp(OperationContext* opCtx); + + /** + * Fetches the UUID of the transaction table, or an empty optional if the collection does not + * exist or has no UUID. Acquires a lock on the collection. + * + * Required for rollback via refetch. + */ + static boost::optional<UUID> getTransactionTableUUID(OperationContext* opCtx); + + /** + * Callback to be invoked in response to insert/update/delete of 'config.transactions' in order + * to notify the session catalog that the on-disk contents are out of sync with the in-memory + * state. The 'singleSessionDoc' must contain the _id of the session which was updated. + */ + static void observeDirectWriteToConfigTransactions(OperationContext* opCtx, + BSONObj singleSessionDoc); + + /** + * Callback to be invoked when the contents of 'config.transactions' are out of sync with that + * in the in-memory catalog, such as when rollback happens or drop of 'config.transactions'. + */ + static void invalidateAllSessions(OperationContext* opCtx); + + /** + * Locates session entries from the in-memory catalog and in 'config.transactions' which have + * not been referenced before 'possiblyExpired' and deletes them. + * + * Returns the number of sessions, which were reaped from the persisted store on disk. + */ + static int reapSessionsOlderThan(OperationContext* opCtx, + SessionsCollection& sessionsCollection, + Date_t possiblyExpired); + + /** + * Deletes the given session ids from config.transactions and config.image_collection. + */ + static int removeSessionsTransactionRecords(OperationContext* opCtx, + const std::vector<LogicalSessionId>& lsidsToRemove); +}; + +/** + * 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 MongoDOperationContextSession { +public: + MongoDOperationContextSession(OperationContext* opCtx); + ~MongoDOperationContextSession(); + + /** + * This method takes an operation context with a checked-out session and allows it to be + * temporarily or permanently checked back in, in order to allow other operations to use it. + * + * May only be called if the session has actually been checked out previously. + */ + static void checkIn(OperationContext* opCtx, OperationContextSession::CheckInReason reason); + + /** + * May only be called if the session is not checked out already. + */ + static void checkOut(OperationContext* opCtx); + +private: + OperationContextSession _operationContextSession; +}; + +/** + * Similar to MongoDOperationContextSession, but marks the TransactionParticipant as valid without + * refreshing from disk and starts a new transaction unconditionally. + * + * NOTE: Only used by the replication oplog application logic on secondaries in order to replay + * prepared transactions. + */ +class MongoDOperationContextSessionWithoutRefresh { +public: + MongoDOperationContextSessionWithoutRefresh(OperationContext* opCtx); + ~MongoDOperationContextSessionWithoutRefresh(); + +private: + OperationContextSession _operationContextSession; + OperationContext* const _opCtx; +}; + +/** + * Similar to MongoDOperationContextSession, but marks the TransactionParticipant as valid without + * loading the retryable write oplog history. If the last operation was a multi-document + * transaction, is equivalent to MongoDOperationContextSession. + * + * NOTE: Should only be used when reading the oplog history is not possible. + */ +class MongoDOperationContextSessionWithoutOplogRead { +public: + MongoDOperationContextSessionWithoutOplogRead(OperationContext* opCtx); + ~MongoDOperationContextSessionWithoutOplogRead(); + +private: + OperationContextSession _operationContextSession; + OperationContext* const _opCtx; +}; + +} // namespace mongo diff --git a/src/mongo/db/session/session_catalog_mongod_test.cpp b/src/mongo/db/session/session_catalog_mongod_test.cpp new file mode 100644 index 00000000000..29acb444f56 --- /dev/null +++ b/src/mongo/db/session/session_catalog_mongod_test.cpp @@ -0,0 +1,93 @@ +/** + * Copyright (C) 2019-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * 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 + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * 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 Server Side 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/dbdirectclient.h" +#include "mongo/db/repl/replication_coordinator_mock.h" +#include "mongo/db/service_context_d_test_fixture.h" +#include "mongo/db/session/session_catalog_mongod.h" +#include "mongo/db/session/session_txn_record_gen.h" +#include "mongo/db/session/sessions_collection_mock.h" +#include "mongo/util/clock_source_mock.h" + +namespace mongo { +namespace { + +class MongoDSessionCatalogTest : public ServiceContextMongoDTest { +protected: + MongoDSessionCatalogTest() : ServiceContextMongoDTest(Options{}.useMockClock(true)) {} + + void setUp() override { + ServiceContextMongoDTest::setUp(); + const auto service = getServiceContext(); + auto replCoord = std::make_unique<repl::ReplicationCoordinatorMock>(service); + ASSERT_OK(replCoord->setFollowerMode(repl::MemberState::RS_PRIMARY)); + + repl::ReplicationCoordinator::set(service, std::move(replCoord)); + repl::createOplog(_opCtx); + } + + ClockSourceMock* clock() { + return dynamic_cast<ClockSourceMock*>(getServiceContext()->getFastClockSource()); + } + + ServiceContext::UniqueOperationContext _uniqueOpCtx{makeOperationContext()}; + OperationContext* _opCtx{_uniqueOpCtx.get()}; + + std::shared_ptr<MockSessionsCollectionImpl> _collectionMock{ + std::make_shared<MockSessionsCollectionImpl>()}; + + std::shared_ptr<SessionsCollection> _collection{ + std::make_shared<MockSessionsCollection>(_collectionMock)}; +}; + +TEST_F(MongoDSessionCatalogTest, ReapSomeExpiredSomeNot) { + // Create some "old" sessions + DBDirectClient client(_opCtx); + SessionTxnRecord txn1( + makeLogicalSessionIdForTest(), 100, repl::OpTime(Timestamp(100), 1), clock()->now()); + SessionTxnRecord txn2( + makeLogicalSessionIdForTest(), 200, repl::OpTime(Timestamp(200), 1), clock()->now()); + + client.insert(NamespaceString::kSessionTransactionsTableNamespace.ns(), + std::vector{txn1.toBSON(), txn2.toBSON()}); + + // Add some "new" sessions to ensure they don't get reaped + clock()->advance(Minutes{31}); + _collectionMock->add(LogicalSessionRecord(makeLogicalSessionIdForTest(), clock()->now())); + _collectionMock->add(LogicalSessionRecord(makeLogicalSessionIdForTest(), clock()->now())); + + ASSERT_EQ(2, + MongoDSessionCatalog::reapSessionsOlderThan( + _opCtx, *_collection, clock()->now() - Minutes{30})); +} + +} // namespace +} // namespace mongo diff --git a/src/mongo/db/session/session_catalog_test.cpp b/src/mongo/db/session/session_catalog_test.cpp new file mode 100644 index 00000000000..cb400befde9 --- /dev/null +++ b/src/mongo/db/session/session_catalog_test.cpp @@ -0,0 +1,1658 @@ +/** + * Copyright (C) 2018-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * 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 + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * 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 Server Side 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 <memory> + +#include "mongo/db/cancelable_operation_context.h" +#include "mongo/db/service_context_test_fixture.h" +#include "mongo/db/session/session_catalog.h" +#include "mongo/idl/server_parameter_test_util.h" +#include "mongo/logv2/log.h" +#include "mongo/stdx/future.h" +#include "mongo/unittest/barrier.h" +#include "mongo/unittest/death_test.h" +#include "mongo/unittest/unittest.h" +#include "mongo/util/concurrency/thread_pool.h" +#include "mongo/util/scopeguard.h" + +#define MONGO_LOGV2_DEFAULT_COMPONENT ::mongo::logv2::LogComponent::kDefault + + +namespace mongo { +namespace { + +class SessionCatalogTest : public ServiceContextTest { +protected: + SessionCatalog* catalog() { + return SessionCatalog::get(getServiceContext()); + } + + void assertCanCheckoutSession(const LogicalSessionId& lsid) { + auto opCtx = makeOperationContext(); + opCtx->setLogicalSessionId(lsid); + OperationContextSession ocs(opCtx.get()); + } + + void assertSessionCheckoutTimesOut(const LogicalSessionId& lsid) { + auto opCtx = makeOperationContext(); + opCtx->setLogicalSessionId(lsid); + opCtx->setDeadlineAfterNowBy(Milliseconds(10), ErrorCodes::MaxTimeMSExpired); + ASSERT_THROWS_CODE( + OperationContextSession(opCtx.get()), AssertionException, ErrorCodes::MaxTimeMSExpired); + } + + void assertConcurrentCheckoutTimesOut(const LogicalSessionId& lsid) { + auto future = stdx::async(stdx::launch::async, [this, lsid] { + ThreadClient tc(getServiceContext()); + auto sideOpCtx = Client::getCurrent()->makeOperationContext(); + sideOpCtx->setLogicalSessionId(lsid); + sideOpCtx->setDeadlineAfterNowBy(Milliseconds(10), ErrorCodes::MaxTimeMSExpired); + OperationContextSession ocs(sideOpCtx.get()); + }); + ASSERT_THROWS_CODE(future.get(), AssertionException, ErrorCodes::MaxTimeMSExpired); + } + + /** + * Creates the session with the given 'lsid' by checking it out from the SessionCatalog and then + * checking it back in. + */ + void createSession(const LogicalSessionId& lsid) { + stdx::async(stdx::launch::async, + [this, lsid] { + ThreadClient tc(getServiceContext()); + auto opCtx = makeOperationContext(); + opCtx->setLogicalSessionId(lsid); + OperationContextSession ocs(opCtx.get()); + }) + .get(); + } + + /** + * Returns the session ids for all sessions in the SessionCatalog. + */ + std::vector<LogicalSessionId> getAllSessionIds(OperationContext* opCtx) { + std::vector<LogicalSessionId> lsidsFound; + SessionKiller::Matcher matcherAllSessions( + KillAllSessionsByPatternSet{makeKillAllSessionsByPattern(opCtx)}); + const auto getAllSessionIdsWorkerFn = [&lsidsFound](const ObservableSession& session) { + lsidsFound.push_back(session.getSessionId()); + }; + catalog()->scanSessions(matcherAllSessions, getAllSessionIdsWorkerFn); + return lsidsFound; + }; +}; + +class SessionCatalogTestWithDefaultOpCtx : public SessionCatalogTest { +protected: + const ServiceContext::UniqueOperationContext _uniqueOpCtx = makeOperationContext(); + OperationContext* const _opCtx = _uniqueOpCtx.get(); +}; + +// 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; +}; + +TEST_F(SessionCatalogTest, GetParentSessionId) { + auto parentLsid = makeLogicalSessionIdForTest(); + ASSERT(!getParentSessionId(parentLsid).has_value()); + ASSERT_EQ(parentLsid, + *getParentSessionId(makeLogicalSessionIdWithTxnNumberAndUUIDForTest(parentLsid))); + ASSERT_EQ(parentLsid, *getParentSessionId(makeLogicalSessionIdWithTxnUUIDForTest(parentLsid))); +} + +TEST_F(SessionCatalogTestWithDefaultOpCtx, CheckoutAndReleaseSession) { + _opCtx->setLogicalSessionId(makeLogicalSessionIdForTest()); + OperationContextSession ocs(_opCtx); + + auto session = OperationContextSession::get(_opCtx); + ASSERT(session); + ASSERT_EQ(*_opCtx->getLogicalSessionId(), session->getSessionId()); +} + +TEST_F(SessionCatalogTestWithDefaultOpCtx, CheckoutAndReleaseSessionWithTxnNumber) { + auto parentLsid = makeLogicalSessionIdForTest(); + auto childLsid = makeLogicalSessionIdWithTxnNumberAndUUIDForTest(parentLsid); + _opCtx->setLogicalSessionId(childLsid); + OperationContextSession ocs(_opCtx); + + auto session = OperationContextSession::get(_opCtx); + auto parentSession = session->getParentSession(); + ASSERT(session); + ASSERT_EQ(childLsid, session->getSessionId()); + ASSERT(parentSession); + ASSERT_EQ(parentLsid, parentSession->getSessionId()); +} + +TEST_F(SessionCatalogTestWithDefaultOpCtx, CheckoutAndReleaseSessionWithTxnUUID) { + auto parentLsid = makeLogicalSessionIdForTest(); + auto childLsid = makeLogicalSessionIdWithTxnUUIDForTest(parentLsid); + _opCtx->setLogicalSessionId(childLsid); + OperationContextSession ocs(_opCtx); + + auto session = OperationContextSession::get(_opCtx); + auto parentSession = session->getParentSession(); + ASSERT(session); + ASSERT_EQ(childLsid, session->getSessionId()); + ASSERT(parentSession); + ASSERT_EQ(parentLsid, parentSession->getSessionId()); +} + +TEST_F(SessionCatalogTestWithDefaultOpCtx, CannotCheckOutParentSessionOfCheckedOutSession) { + auto runTest = [&](const LogicalSessionId& parentLsid, const LogicalSessionId& childLsid) { + _opCtx->setLogicalSessionId(childLsid); + OperationContextSession ocs(_opCtx); + + // Verify that the parent session cannot be checked out until the child session is checked + // back in. + auto future = stdx::async(stdx::launch::async, [this, parentLsid] { + ThreadClient tc(getServiceContext()); + auto opCtx = cc().makeOperationContext(); + opCtx->setLogicalSessionId(parentLsid); + OperationContextSession ocs(opCtx.get()); + }); + ASSERT(stdx::future_status::ready != future.wait_for(Milliseconds(10).toSystemDuration())); + + OperationContextSession::checkIn(_opCtx, OperationContextSession::CheckInReason::kDone); + ASSERT(!OperationContextSession::get(_opCtx)); + future.get(); + }; + + auto parentLsid = makeLogicalSessionIdForTest(); + runTest(parentLsid, makeLogicalSessionIdWithTxnNumberAndUUIDForTest(parentLsid)); + runTest(parentLsid, makeLogicalSessionIdWithTxnUUIDForTest(parentLsid)); +} + +TEST_F(SessionCatalogTestWithDefaultOpCtx, CannotCheckOutChildSessionOfCheckedOutSession) { + auto runTest = [&](const LogicalSessionId& parentLsid, const LogicalSessionId& childLsid) { + _opCtx->setLogicalSessionId(parentLsid); + OperationContextSession ocs(_opCtx); + + // Verify that the child session cannot be checked out until the parent session is checked + // back in. + auto future = stdx::async(stdx::launch::async, [this, childLsid] { + ThreadClient tc(getServiceContext()); + auto opCtx = cc().makeOperationContext(); + opCtx->setLogicalSessionId(childLsid); + OperationContextSession ocs(opCtx.get()); + }); + ASSERT(stdx::future_status::ready != future.wait_for(Milliseconds(10).toSystemDuration())); + + OperationContextSession::checkIn(_opCtx, OperationContextSession::CheckInReason::kDone); + ASSERT(!OperationContextSession::get(_opCtx)); + future.get(); + }; + + auto parentLsid = makeLogicalSessionIdForTest(); + runTest(parentLsid, makeLogicalSessionIdWithTxnNumberAndUUIDForTest(parentLsid)); + runTest(parentLsid, makeLogicalSessionIdWithTxnUUIDForTest(parentLsid)); +} + +TEST_F(SessionCatalogTestWithDefaultOpCtx, CannotCheckoutMultipleChildSessionsConcurrently) { + auto runTest = [&](const LogicalSessionId& childLsid0, const LogicalSessionId& childLsid1) { + _opCtx->setLogicalSessionId(childLsid0); + OperationContextSession ocs(_opCtx); + + // Verify that another child session cannot be checked out until both the child session + // above and the parent session are checked back in. + auto future = stdx::async(stdx::launch::async, [this, childLsid1] { + ThreadClient tc(getServiceContext()); + auto childSessionOpCtx1 = cc().makeOperationContext(); + childSessionOpCtx1->setLogicalSessionId(childLsid1); + OperationContextSession ocs(childSessionOpCtx1.get()); + }); + ASSERT(stdx::future_status::ready != future.wait_for(Milliseconds(10).toSystemDuration())); + + OperationContextSession::checkIn(_opCtx, OperationContextSession::CheckInReason::kDone); + ASSERT(!OperationContextSession::get(_opCtx)); + future.get(); + }; + + auto parentLsid = makeLogicalSessionIdForTest(); + runTest(makeLogicalSessionIdWithTxnNumberAndUUIDForTest(parentLsid), + makeLogicalSessionIdWithTxnNumberAndUUIDForTest(parentLsid)); + runTest(makeLogicalSessionIdWithTxnUUIDForTest(parentLsid), + makeLogicalSessionIdWithTxnUUIDForTest(parentLsid)); + runTest(makeLogicalSessionIdWithTxnNumberAndUUIDForTest(parentLsid), + makeLogicalSessionIdWithTxnUUIDForTest(parentLsid)); +} + +TEST_F(SessionCatalogTestWithDefaultOpCtx, OperationContextCheckedOutSession) { + _opCtx->setLogicalSessionId(makeLogicalSessionIdForTest()); + const TxnNumber txnNum = 20; + _opCtx->setTxnNumber(txnNum); + + OperationContextSession ocs(_opCtx); + auto session = OperationContextSession::get(_opCtx); + ASSERT(session); + ASSERT_EQ(*_opCtx->getLogicalSessionId(), session->getSessionId()); +} + +TEST_F(SessionCatalogTestWithDefaultOpCtx, NestedOperationContextSession) { + auto runTest = [&](const LogicalSessionId& lsid) { + _opCtx->setLogicalSessionId(lsid); + + { + OperationContextSession outerScopedSession(_opCtx); + + { + DirectClientSetter inDirectClient(_opCtx); + OperationContextSession innerScopedSession(_opCtx); + + auto session = OperationContextSession::get(_opCtx); + ASSERT(session); + ASSERT_EQ(*_opCtx->getLogicalSessionId(), session->getSessionId()); + } + + { + DirectClientSetter inDirectClient(_opCtx); + auto session = OperationContextSession::get(_opCtx); + ASSERT(session); + ASSERT_EQ(*_opCtx->getLogicalSessionId(), session->getSessionId()); + } + } + + ASSERT(!OperationContextSession::get(_opCtx)); + }; + + runTest(makeLogicalSessionIdForTest()); + runTest(makeLogicalSessionIdWithTxnNumberAndUUIDForTest()); + runTest(makeLogicalSessionIdWithTxnUUIDForTest()); +} + +TEST_F(SessionCatalogTest, ScanSession) { + // Create sessions in the catalog. + const auto& lsids = []() -> std::vector<LogicalSessionId> { + auto lsid0 = makeLogicalSessionIdForTest(); + auto lsid1 = makeLogicalSessionIdForTest(); + auto lsid2 = makeLogicalSessionIdWithTxnNumberAndUUIDForTest(lsid1); + auto lsid3 = makeLogicalSessionIdWithTxnUUIDForTest(lsid1); + return {lsid0, lsid1, lsid2, lsid3}; + }(); + for (const auto& lsid : lsids) { + stdx::async(stdx::launch::async, + [this, lsid] { + ThreadClient tc(getServiceContext()); + auto opCtx = makeOperationContext(); + opCtx->setLogicalSessionId(lsid); + OperationContextSession ocs(opCtx.get()); + }) + .get(); + } + + catalog()->scanSession(lsids[0], [&lsids](const ObservableSession& session) { + ASSERT_EQ(lsids[0], session.get()->getSessionId()); + }); + + catalog()->scanSession(lsids[1], [&lsids](const ObservableSession& session) { + ASSERT_EQ(lsids[1], session.get()->getSessionId()); + }); + + catalog()->scanSession(lsids[2], [&lsids](const ObservableSession& session) { + ASSERT_EQ(lsids[2], session.get()->getSessionId()); + }); + + catalog()->scanSession(lsids[3], [&lsids](const ObservableSession& session) { + ASSERT_EQ(lsids[3], session.get()->getSessionId()); + }); + + catalog()->scanSession(makeLogicalSessionIdForTest(), [](const ObservableSession&) { + FAIL("The callback was called for non-existent session"); + }); +} + +TEST_F(SessionCatalogTestWithDefaultOpCtx, ScanSessionsForReapWhenSessionIsIdle) { + auto parentLsid = makeLogicalSessionIdForTest(); + auto childLsid0 = makeLogicalSessionIdWithTxnUUIDForTest(parentLsid); + auto childLsid1 = makeLogicalSessionIdWithTxnNumberAndUUIDForTest(parentLsid); + auto otherParentLsid = makeLogicalSessionIdForTest(); + + createSession(parentLsid); + createSession(childLsid0); + createSession(childLsid1); + createSession(otherParentLsid); + auto lsidsFound = getAllSessionIds(_opCtx); + ASSERT_EQ(4U, lsidsFound.size()); + + // Mark otherParentSession for reap. The session should get reaped since it doesn't have any + // child session. + auto lsidsNotReaped = catalog()->scanSessionsForReap( + otherParentLsid, + [](ObservableSession& parentSession) { + parentSession.markForReap(ObservableSession::ReapMode::kNonExclusive); + }, + [](ObservableSession& childSession) {}); + lsidsFound = getAllSessionIds(_opCtx); + ASSERT_EQ(3U, lsidsFound.size()); + catalog()->scanSession(otherParentLsid, [](const ObservableSession&) { + FAIL("Found a session that should have been reaped"); + }); + ASSERT_EQ(0U, lsidsNotReaped.size()); + + // Mark parentSession for reap. The session should not get reaped since its child sessions + // (i.e. childSession0 and childSession1) are not marked for reaped. + lsidsNotReaped = catalog()->scanSessionsForReap( + parentLsid, + [](ObservableSession& parentSession) { + parentSession.markForReap(ObservableSession::ReapMode::kNonExclusive); + }, + [](ObservableSession& childSession) {}); + lsidsFound = getAllSessionIds(_opCtx); + ASSERT_EQ(3U, lsidsFound.size()); + ASSERT_EQ(lsidsFound.size(), lsidsNotReaped.size()); + for (const auto& lsid : lsidsFound) { + ASSERT(lsidsNotReaped.find(lsid) != lsidsNotReaped.end()); + } + + // Mark childSession0 and childSession1 for reap with kNonExclusive mode. The sessions should + // not get reaped since parentSession is not marked for reaped. + lsidsNotReaped = catalog()->scanSessionsForReap( + parentLsid, + [](ObservableSession& parentSession) { + + }, + [](ObservableSession& childSession) { + childSession.markForReap(ObservableSession::ReapMode::kNonExclusive); + }); + lsidsFound = getAllSessionIds(_opCtx); + ASSERT_EQ(3U, lsidsFound.size()); + ASSERT_EQ(lsidsFound.size(), lsidsNotReaped.size()); + for (const auto& lsid : lsidsFound) { + ASSERT(lsidsNotReaped.find(lsid) != lsidsNotReaped.end()); + } + + // Mark childSession0 for reap with kExclusive mode. The session should get reaped. + lsidsNotReaped = catalog()->scanSessionsForReap( + parentLsid, + [](ObservableSession& parentSession) {}, + [&](ObservableSession& childSession) { + if (childSession.getSessionId() == childLsid0) { + childSession.markForReap(ObservableSession::ReapMode::kExclusive); + } + }); + lsidsFound = getAllSessionIds(_opCtx); + ASSERT_EQ(2U, lsidsFound.size()); + catalog()->scanSession(childLsid0, [](const ObservableSession&) { + FAIL("Found a session that should have been reaped"); + }); + ASSERT_EQ(lsidsFound.size(), lsidsNotReaped.size()); + for (const auto& lsid : lsidsFound) { + ASSERT(lsidsNotReaped.find(lsid) != lsidsNotReaped.end()); + } + + // Mark parentSession and childSession1 for reap with kNonExclusive mode. Both sessions should + // get reaped since all sessions are now marked for reap. + lsidsNotReaped = catalog()->scanSessionsForReap( + parentLsid, + [](ObservableSession& parentSession) { + parentSession.markForReap(ObservableSession::ReapMode::kNonExclusive); + }, + [](ObservableSession& childSession) { + childSession.markForReap(ObservableSession::ReapMode::kNonExclusive); + }); + + lsidsFound = getAllSessionIds(_opCtx); + ASSERT_EQ(0U, lsidsFound.size()); + ASSERT_EQ(lsidsFound.size(), lsidsNotReaped.size()); +} + +DEATH_TEST_F(SessionCatalogTestWithDefaultOpCtx, + ScanSessionDoesNotSupportReaping, + "Cannot reap a session via 'scanSession'") { + auto lsid = makeLogicalSessionIdForTest(); + + { + _opCtx->setLogicalSessionId(lsid); + OperationContextSession ocs(_opCtx); + } + + catalog()->scanSession(lsid, [](ObservableSession& session) { + session.markForReap(ObservableSession::ReapMode::kNonExclusive); + }); +} + +TEST_F(SessionCatalogTestWithDefaultOpCtx, ScanSessions) { + std::vector<LogicalSessionId> lsidsFound; + const auto workerFn = [&lsidsFound](const ObservableSession& session) { + lsidsFound.push_back(session.getSessionId()); + }; + + // Scan over zero Sessions. + SessionKiller::Matcher matcherAllSessions( + KillAllSessionsByPatternSet{makeKillAllSessionsByPattern(_opCtx)}); + catalog()->scanSessions(matcherAllSessions, workerFn); + ASSERT(lsidsFound.empty()); + lsidsFound.clear(); + + // Create sessions in the catalog. + const auto& lsids = []() -> std::vector<LogicalSessionId> { + auto lsid0 = makeLogicalSessionIdForTest(); + auto lsid1 = makeLogicalSessionIdForTest(); + auto lsid2 = makeLogicalSessionIdWithTxnNumberAndUUIDForTest(lsid1); + auto lsid3 = makeLogicalSessionIdWithTxnUUIDForTest(lsid1); + return {lsid0, lsid1, lsid2, lsid3}; + }(); + for (const auto& lsid : lsids) { + stdx::async(stdx::launch::async, + [this, lsid] { + ThreadClient tc(getServiceContext()); + auto opCtx = makeOperationContext(); + opCtx->setLogicalSessionId(lsid); + OperationContextSession ocs(opCtx.get()); + }) + .get(); + } + + // Scan over all Sessions. + catalog()->scanSessions(matcherAllSessions, workerFn); + ASSERT_EQ(4U, lsidsFound.size()); + lsidsFound.clear(); + + // Scan over all Sessions, visiting a Session with child Sessions. + SessionKiller::Matcher matcherLSID1( + KillAllSessionsByPatternSet{makeKillAllSessionsByPattern(_opCtx, lsids[1])}); + catalog()->scanSessions(matcherLSID1, workerFn); + + ASSERT_EQ(3U, lsidsFound.size()); + + const auto searchLsidsFound = [&](const LogicalSessionId lsid) { + return std::find(lsidsFound.begin(), lsidsFound.end(), lsid) != lsidsFound.end(); + }; + + for (size_t i = 1; i < lsids.size(); ++i) { + if (!searchLsidsFound(lsids[i])) { + FAIL("Match missed an lsid"); + } + } + lsidsFound.clear(); + + // Do not allow matching on child sessions. + ASSERT_THROWS_CODE(KillAllSessionsByPatternSet{makeKillAllSessionsByPattern(_opCtx, lsids[2])}, + AssertionException, + ErrorCodes::InvalidOptions); +} + +TEST_F(SessionCatalogTestWithDefaultOpCtx, ScanSessionsForReapWhenParentSessionIsCheckedOut) { + auto runTest = [&](bool hangAfterIncrementingNumWaitingToCheckOut) { + auto parentLsid = makeLogicalSessionIdForTest(); + auto childLsid0 = makeLogicalSessionIdWithTxnUUIDForTest(parentLsid); + auto childLsid1 = makeLogicalSessionIdWithTxnNumberAndUUIDForTest(parentLsid); + + createSession(parentLsid); + createSession(childLsid0); + createSession(childLsid1); + auto lsidsFound = getAllSessionIds(_opCtx); + ASSERT_EQ(3U, lsidsFound.size()); + + unittest::Barrier sessionsCheckedOut(2); + unittest::Barrier sessionsCheckedIn(2); + + // Check out parentSession. + auto future = stdx::async(stdx::launch::async, [&] { + ThreadClient tc(getServiceContext()); + auto opCtx = makeOperationContext(); + + if (hangAfterIncrementingNumWaitingToCheckOut) { + auto fp = + globalFailPointRegistry().find("hangAfterIncrementingNumWaitingToCheckOut"); + auto initialTimesEntered = fp->setMode(FailPoint::alwaysOn); + + auto innerFuture = stdx::async(stdx::launch::async, [&] { + ThreadClient innerTc(getServiceContext()); + auto innerOpCtx = makeOperationContext(); + innerOpCtx->setLogicalSessionId(parentLsid); + OperationContextSession ocs(innerOpCtx.get()); + }); + + fp->waitForTimesEntered(opCtx.get(), initialTimesEntered + 1); + sessionsCheckedOut.countDownAndWait(); + sessionsCheckedIn.countDownAndWait(); + fp->setMode(FailPoint::off); + innerFuture.get(); + } else { + opCtx->setLogicalSessionId(parentLsid); + OperationContextSession ocs(opCtx.get()); + sessionsCheckedOut.countDownAndWait(); + sessionsCheckedIn.countDownAndWait(); + } + }); + // After this wait, parentSession is either checked out or has a thread waiting for it be + // checked out. + sessionsCheckedOut.countDownAndWait(); + + // Mark parentSession for reap, and additionally mark childSession0 and childSession1 for + // reap with kNonExclusive mode. parentSession should not get reaped because it is checked + // out or has a thread waiting for it be checked out. childSession0 and childSession1 also + // should not get reaped since they must be reaped with parentSession. + auto lsidsNotReaped = catalog()->scanSessionsForReap( + parentLsid, + [](ObservableSession& parentSession) { + parentSession.markForReap(ObservableSession::ReapMode::kNonExclusive); + }, + [](ObservableSession& childSession) { + childSession.markForReap(ObservableSession::ReapMode::kNonExclusive); + }); + lsidsFound = getAllSessionIds(_opCtx); + ASSERT_EQ(3U, lsidsFound.size()); + ASSERT_EQ(lsidsFound.size(), lsidsNotReaped.size()); + for (const auto& lsid : lsidsFound) { + ASSERT(lsidsNotReaped.find(lsid) != lsidsNotReaped.end()); + } + + // Mark childSession0 for reap with kExclusive mode. It should get reaped although + // parentSession is checked out or has a thread waiting for it be checked out. + lsidsNotReaped = catalog()->scanSessionsForReap( + parentLsid, + [](ObservableSession& parentSession) {}, + [&](ObservableSession& childSession) { + if (childSession.getSessionId() == childLsid0) { + childSession.markForReap(ObservableSession::ReapMode::kExclusive); + } + }); + lsidsFound = getAllSessionIds(_opCtx); + ASSERT_EQ(2U, lsidsFound.size()); + catalog()->scanSession(childLsid0, [](const ObservableSession&) { + FAIL("Found a session that should have been reaped"); + }); + ASSERT_EQ(lsidsFound.size(), lsidsNotReaped.size()); + for (const auto& lsid : lsidsFound) { + ASSERT(lsidsNotReaped.find(lsid) != lsidsNotReaped.end()); + } + + // Mark childSession1 for reap with mode kExclusive. The session should get reaped although + // parentSession is checked out or has a thread waiting for it be checked out. + lsidsNotReaped = catalog()->scanSessionsForReap( + parentLsid, + [](ObservableSession& parentSession) {}, + [](ObservableSession& childSession) { + childSession.markForReap(ObservableSession::ReapMode::kExclusive); + }); + lsidsFound = getAllSessionIds(_opCtx); + ASSERT_EQ(1U, lsidsFound.size()); + catalog()->scanSession(childLsid1, [](const ObservableSession&) { + FAIL("Found a session that should have been reaped"); + }); + ASSERT_EQ(lsidsFound.size(), lsidsNotReaped.size()); + for (const auto& lsid : lsidsFound) { + ASSERT(lsidsNotReaped.find(lsid) != lsidsNotReaped.end()); + } + + // After this point, parentSession is checked back in. + sessionsCheckedIn.countDownAndWait(); + future.get(); + + // Mark parentSession for reap. The session should now get reaped. + lsidsNotReaped = catalog()->scanSessionsForReap( + parentLsid, + [](ObservableSession& parentSession) { + parentSession.markForReap(ObservableSession::ReapMode::kNonExclusive); + }, + [](ObservableSession& childSession) { + + }); + + lsidsFound = getAllSessionIds(_opCtx); + ASSERT_EQ(0U, lsidsFound.size()); + ASSERT_EQ(lsidsFound.size(), lsidsNotReaped.size()); + }; + + runTest(false /* hangAfterIncrementingNumWaitingToCheckOut */); + runTest(true /* hangAfterIncrementingNumWaitingToCheckOut */); +} + +TEST_F(SessionCatalogTestWithDefaultOpCtx, ScanSessionsForReapWhenChildSessionIsCheckedOut) { + auto runTest = [&](bool hangAfterIncrementingNumWaitingToCheckOut) { + auto parentLsid = makeLogicalSessionIdForTest(); + auto parentTxnNumber = TxnNumber{0}; + auto childLsid0 = makeLogicalSessionIdWithTxnUUIDForTest(parentLsid); + auto childLsid1 = + makeLogicalSessionIdWithTxnNumberAndUUIDForTest(parentLsid, parentTxnNumber++); + auto childLsid2 = + makeLogicalSessionIdWithTxnNumberAndUUIDForTest(parentLsid, parentTxnNumber); + + createSession(parentLsid); + createSession(childLsid0); + createSession(childLsid1); + createSession(childLsid2); + auto lsidsFound = getAllSessionIds(_opCtx); + ASSERT_EQ(4U, lsidsFound.size()); + + unittest::Barrier sessionsCheckedOut(2); + unittest::Barrier sessionsCheckedIn(2); + + // Check out childSession2. + auto future = stdx::async(stdx::launch::async, [&] { + ThreadClient tc(getServiceContext()); + auto opCtx = makeOperationContext(); + + if (hangAfterIncrementingNumWaitingToCheckOut) { + auto fp = + globalFailPointRegistry().find("hangAfterIncrementingNumWaitingToCheckOut"); + auto initialTimesEntered = fp->setMode(FailPoint::alwaysOn); + + auto innerFuture = stdx::async(stdx::launch::async, [&] { + ThreadClient innerTc(getServiceContext()); + auto innerOpCtx = makeOperationContext(); + innerOpCtx->setLogicalSessionId(childLsid2); + OperationContextSession ocs(innerOpCtx.get()); + }); + + fp->waitForTimesEntered(opCtx.get(), initialTimesEntered + 1); + sessionsCheckedOut.countDownAndWait(); + sessionsCheckedIn.countDownAndWait(); + fp->setMode(FailPoint::off); + innerFuture.get(); + } else { + opCtx->setLogicalSessionId(childLsid2); + OperationContextSession ocs(opCtx.get()); + sessionsCheckedOut.countDownAndWait(); + sessionsCheckedIn.countDownAndWait(); + } + }); + // After this wait, childSession2 is either checked out or has a thread waiting for it be + // checked out. + sessionsCheckedOut.countDownAndWait(); + + // Mark childSession2 for reap with kExclusive mode. The session should not get reaped since + // it is checked out or has a thread waiting for it be checked out. + auto lsidsNotReaped = catalog()->scanSessionsForReap( + parentLsid, + [](ObservableSession& parentSession) {}, + [&](ObservableSession& childSession) { + if (childSession.getSessionId() == childLsid2) { + childSession.markForReap(ObservableSession::ReapMode::kExclusive); + } + }); + lsidsFound = getAllSessionIds(_opCtx); + ASSERT_EQ(4U, lsidsFound.size()); + ASSERT_EQ(lsidsFound.size(), lsidsNotReaped.size()); + for (const auto& lsid : lsidsFound) { + ASSERT(lsidsNotReaped.find(lsid) != lsidsNotReaped.end()); + } + + // Mark parentSession for reap, and additionally mark Reap childSession0 and childSession1 + // with mode kExclusive. parentSession should not get reaped because childSession2 is + // checked out or has a thread waiting for it be checked out. childSession0 and + // childSession1 should get reaped since they are not checked out or have any threads + // waiting for them be checked out. + lsidsNotReaped = catalog()->scanSessionsForReap( + parentLsid, + [&](ObservableSession& parentSession) { + parentSession.markForReap(ObservableSession::ReapMode::kNonExclusive); + }, + [&](ObservableSession& childSession) { + auto lsid = childSession.getSessionId(); + if (lsid == childLsid0 || lsid == childLsid1) { + childSession.markForReap(ObservableSession::ReapMode::kExclusive); + } + }); + + lsidsFound = getAllSessionIds(_opCtx); + ASSERT_EQ(2U, lsidsFound.size()); + catalog()->scanSession(childLsid0, [](const ObservableSession&) { + FAIL("Found a session that should have been reaped"); + }); + catalog()->scanSession(childLsid1, [](const ObservableSession&) { + FAIL("Found a session that should have been reaped"); + }); + ASSERT_EQ(lsidsFound.size(), lsidsNotReaped.size()); + for (const auto& lsid : lsidsFound) { + ASSERT(lsidsNotReaped.find(lsid) != lsidsNotReaped.end()); + } + + // After this point, childSession2 is checked back in. + sessionsCheckedIn.countDownAndWait(); + future.get(); + + // Mark parentSession and childSession2 for reap with kNonExclusive mode. Both sessions + // should get reaped. + lsidsNotReaped = catalog()->scanSessionsForReap( + parentLsid, + [](ObservableSession& parentSession) { + parentSession.markForReap(ObservableSession::ReapMode::kNonExclusive); + }, + [](ObservableSession& childSession) { + childSession.markForReap(ObservableSession::ReapMode::kNonExclusive); + }); + + lsidsFound = getAllSessionIds(_opCtx); + ASSERT_EQ(0U, lsidsFound.size()); + ASSERT_EQ(lsidsFound.size(), lsidsNotReaped.size()); + }; + + runTest(false /* hangAfterIncrementingNumWaitingToCheckOut */); + runTest(true /* hangAfterIncrementingNumWaitingToCheckOut */); +} + +DEATH_TEST_F(SessionCatalogTestWithDefaultOpCtx, + ScanSessionsDoesNotSupportReaping, + "Cannot reap a session via 'scanSessions'") { + { + auto lsid = makeLogicalSessionIdForTest(); + _opCtx->setLogicalSessionId(lsid); + OperationContextSession ocs(_opCtx); + } + + SessionKiller::Matcher matcherAllSessions( + KillAllSessionsByPatternSet{makeKillAllSessionsByPattern(_opCtx)}); + catalog()->scanSessions(matcherAllSessions, [](ObservableSession& session) { + session.markForReap(ObservableSession::ReapMode::kNonExclusive); + }); +} + +TEST_F(SessionCatalogTest, KillSessionWhenSessionIsNotCheckedOut) { + auto runTest = [&](const LogicalSessionId& lsid) { + // Create the session so there is something to kill + { + auto opCtx = makeOperationContext(); + opCtx->setLogicalSessionId(lsid); + OperationContextSession ocs(opCtx.get()); + } + + auto killToken = catalog()->killSession(lsid); + + // Make sure that regular session check-out will fail because the session is marked as + // killed + assertSessionCheckoutTimesOut(lsid); + + // Schedule a separate "regular operation" thread, which will block on checking-out the + // session, which we will use to confirm that session kill completion actually unblocks + // check-out + auto future = stdx::async(stdx::launch::async, [this, lsid] { + ThreadClient tc(getServiceContext()); + auto sideOpCtx = Client::getCurrent()->makeOperationContext(); + sideOpCtx->setLogicalSessionId(lsid); + OperationContextSession ocs(sideOpCtx.get()); + }); + ASSERT(stdx::future_status::ready != future.wait_for(Milliseconds(10).toSystemDuration())); + + // Make sure that "for kill" session check-out succeeds + { + auto opCtx = makeOperationContext(); + auto scopedSession = + catalog()->checkOutSessionForKill(opCtx.get(), std::move(killToken)); + ASSERT_EQ(opCtx.get(), scopedSession.currentOperation_forTest()); + } + + // Make sure that session check-out after kill succeeds again + { + auto opCtx = makeOperationContext(); + opCtx->setLogicalSessionId(lsid); + OperationContextSession ocs(opCtx.get()); + } + + // Make sure the "regular operation" eventually is able to proceed and use the just killed + // session + future.get(); + }; + + runTest(makeLogicalSessionIdForTest()); + runTest(makeLogicalSessionIdWithTxnNumberAndUUIDForTest()); + runTest(makeLogicalSessionIdWithTxnUUIDForTest()); +} + +TEST_F(SessionCatalogTest, KillSessionWhenSessionIsCheckedOut) { + auto runTest = [&](const LogicalSessionId& lsid) { + auto killToken = [this, &lsid] { + // Create the session so there is something to kill + auto opCtx = makeOperationContext(); + opCtx->setLogicalSessionId(lsid); + OperationContextSession operationContextSession(opCtx.get()); + + auto killToken = catalog()->killSession(lsid); + + // Make sure the owning operation context is interrupted + ASSERT_THROWS_CODE( + opCtx->checkForInterrupt(), AssertionException, ErrorCodes::Interrupted); + + // Make sure that the checkOutForKill call will wait for the owning operation context to + // check the session back in + assertConcurrentCheckoutTimesOut(lsid); + + return killToken; + }(); + + // Make sure that regular session check-out will fail because the session is marked as + // killed + assertSessionCheckoutTimesOut(lsid); + + // Schedule a separate "regular operation" thread, which will block on checking-out the + // session, which we will use to confirm that session kill completion actually unblocks + // check-out + auto future = stdx::async(stdx::launch::async, [this, lsid] { + ThreadClient tc(getServiceContext()); + auto sideOpCtx = Client::getCurrent()->makeOperationContext(); + sideOpCtx->setLogicalSessionId(lsid); + OperationContextSession ocs(sideOpCtx.get()); + }); + ASSERT(stdx::future_status::ready != future.wait_for(Milliseconds(10).toSystemDuration())); + + // Make sure that "for kill" session check-out succeeds + { + auto opCtx = makeOperationContext(); + auto scopedSession = + catalog()->checkOutSessionForKill(opCtx.get(), std::move(killToken)); + ASSERT_EQ(opCtx.get(), scopedSession.currentOperation_forTest()); + } + + // Make sure that session check-out after kill succeeds again + { + auto opCtx = makeOperationContext(); + opCtx->setLogicalSessionId(lsid); + OperationContextSession ocs(opCtx.get()); + } + + // Make sure the "regular operation" eventually is able to proceed and use the just killed + // session + future.get(); + }; + + runTest(makeLogicalSessionIdForTest()); + runTest(makeLogicalSessionIdWithTxnNumberAndUUIDForTest()); + runTest(makeLogicalSessionIdWithTxnUUIDForTest()); +} + +TEST_F(SessionCatalogTest, KillParentSessionWhenChildSessionIsCheckedOut) { + auto runTest = [&](const LogicalSessionId& parentLsid, const LogicalSessionId& childLsid) { + auto killToken = [this, &parentLsid, &childLsid] { + // Create the session so there is something to kill + auto opCtx = makeOperationContext(); + opCtx->setLogicalSessionId(childLsid); + OperationContextSession operationContextSession(opCtx.get()); + + auto killToken = catalog()->killSession(parentLsid); + + // Make sure the owning operation context is interrupted + ASSERT_THROWS_CODE( + opCtx->checkForInterrupt(), AssertionException, ErrorCodes::Interrupted); + + // Make sure that the checkOutForKill call will wait for the owning operation context to + // check the session back in + assertConcurrentCheckoutTimesOut(childLsid); + + return killToken; + }(); + + // Make sure that regular session check-out will fail because the session is marked as + // killed + assertSessionCheckoutTimesOut(childLsid); + + // Schedule a separate "regular operation" thread, which will block on checking-out the + // session, which we will use to confirm that session kill completion actually unblocks + // check-out + auto future = stdx::async(stdx::launch::async, [this, childLsid] { + ThreadClient tc(getServiceContext()); + auto sideOpCtx = Client::getCurrent()->makeOperationContext(); + sideOpCtx->setLogicalSessionId(childLsid); + OperationContextSession ocs(sideOpCtx.get()); + }); + ASSERT(stdx::future_status::ready != future.wait_for(Milliseconds(10).toSystemDuration())); + + // Make sure that "for kill" session check-out succeeds + { + auto opCtx = makeOperationContext(); + auto scopedSession = + catalog()->checkOutSessionForKill(opCtx.get(), std::move(killToken)); + ASSERT_EQ(opCtx.get(), scopedSession.currentOperation_forTest()); + } + + // Make sure that session check-out after kill succeeds again + { + auto opCtx = makeOperationContext(); + opCtx->setLogicalSessionId(childLsid); + OperationContextSession ocs(opCtx.get()); + } + + // Make sure the "regular operation" eventually is able to proceed and use the just killed + // session + future.get(); + }; + + auto parentLsid = makeLogicalSessionIdForTest(); + runTest(parentLsid, makeLogicalSessionIdWithTxnNumberAndUUIDForTest(parentLsid)); + runTest(parentLsid, makeLogicalSessionIdWithTxnUUIDForTest(parentLsid)); +} + +TEST_F(SessionCatalogTest, KillParentSessionWhenChildSessionIsNotCheckedOut) { + auto runTest = [&](const LogicalSessionId& parentLsid, const LogicalSessionId& childLsid) { + auto killToken = [this, &parentLsid, &childLsid] { + // Create the session so there is something to kill + { + auto opCtx = makeOperationContext(); + opCtx->setLogicalSessionId(childLsid); + OperationContextSession operationContextSession(opCtx.get()); + } + + auto killToken = catalog()->killSession(parentLsid); + + // Make sure that the checkOutForKill call will wait for the owning operation context to + // check the session back in + assertConcurrentCheckoutTimesOut(childLsid); + + return killToken; + }(); + + // Make sure that regular session check-out will fail because the session is marked as + // killed + assertSessionCheckoutTimesOut(childLsid); + + // Schedule a separate "regular operation" thread, which will block on checking-out the + // session, which we will use to confirm that session kill completion actually unblocks + // check-out + auto future = stdx::async(stdx::launch::async, [this, childLsid] { + ThreadClient tc(getServiceContext()); + auto sideOpCtx = Client::getCurrent()->makeOperationContext(); + sideOpCtx->setLogicalSessionId(childLsid); + OperationContextSession ocs(sideOpCtx.get()); + }); + ASSERT(stdx::future_status::ready != future.wait_for(Milliseconds(10).toSystemDuration())); + + // Make sure that "for kill" session check-out succeeds + { + auto opCtx = makeOperationContext(); + auto scopedSession = + catalog()->checkOutSessionForKill(opCtx.get(), std::move(killToken)); + ASSERT_EQ(opCtx.get(), scopedSession.currentOperation_forTest()); + } + + // Make sure that session check-out after kill succeeds again + { + auto opCtx = makeOperationContext(); + opCtx->setLogicalSessionId(childLsid); + OperationContextSession ocs(opCtx.get()); + } + + // Make sure the "regular operation" eventually is able to proceed and use the just killed + // session + future.get(); + }; + + auto parentLsid = makeLogicalSessionIdForTest(); + runTest(parentLsid, makeLogicalSessionIdWithTxnNumberAndUUIDForTest(parentLsid)); + runTest(parentLsid, makeLogicalSessionIdWithTxnUUIDForTest(parentLsid)); +} + +TEST_F(SessionCatalogTest, KillSessionWhenChildSessionIsCheckedOut) { + auto runTest = [&](const LogicalSessionId& parentLsid, const LogicalSessionId& childLsid) { + auto killToken = [this, &parentLsid, &childLsid] { + // Create the session so there is something to kill + auto opCtx = makeOperationContext(); + opCtx->setLogicalSessionId(childLsid); + OperationContextSession operationContextSession(opCtx.get()); + + auto killToken = catalog()->killSession(childLsid); + + // Make sure the owning operation context is interrupted + ASSERT_THROWS_CODE( + opCtx->checkForInterrupt(), AssertionException, ErrorCodes::Interrupted); + + // Make sure that the checkOutForKill call will wait for the owning operation context to + // check the session back in + assertConcurrentCheckoutTimesOut(parentLsid); + assertConcurrentCheckoutTimesOut(childLsid); + + return killToken; + }(); + + // Make sure that regular session check-out will fail because the session is marked as + // killed + assertSessionCheckoutTimesOut(childLsid); + + // Check that checking out the parent session will fail because it was marked as killed. + assertSessionCheckoutTimesOut(parentLsid); + + // Schedule a separate "regular operation" thread, which will block on checking-out the + // session, which we will use to confirm that session kill completion actually unblocks + // check-out + auto childFuture = stdx::async(stdx::launch::async, [this, childLsid] { + ThreadClient tc(getServiceContext()); + auto sideOpCtx = Client::getCurrent()->makeOperationContext(); + sideOpCtx->setLogicalSessionId(childLsid); + OperationContextSession ocs(sideOpCtx.get()); + }); + ASSERT(stdx::future_status::ready != + childFuture.wait_for(Milliseconds(10).toSystemDuration())); + + auto parentFuture = stdx::async(stdx::launch::async, [this, parentLsid] { + ThreadClient tc(getServiceContext()); + auto sideOpCtx = Client::getCurrent()->makeOperationContext(); + sideOpCtx->setLogicalSessionId(parentLsid); + OperationContextSession ocs(sideOpCtx.get()); + }); + ASSERT(stdx::future_status::ready != + parentFuture.wait_for(Milliseconds(10).toSystemDuration())); + + // Make sure that "for kill" session check-out succeeds + { + auto opCtx = makeOperationContext(); + auto scopedSession = + catalog()->checkOutSessionForKill(opCtx.get(), std::move(killToken)); + ASSERT_EQ(opCtx.get(), scopedSession.currentOperation_forTest()); + } + + // Make sure that session check-out after kill succeeds again + assertCanCheckoutSession(childLsid); + assertCanCheckoutSession(parentLsid); + + // Make sure the "regular operations" eventually proceed and use the just killed session + childFuture.get(); + parentFuture.get(); + }; + + auto parentLsid = makeLogicalSessionIdForTest(); + runTest(parentLsid, makeLogicalSessionIdWithTxnNumberAndUUIDForTest(parentLsid)); + runTest(parentLsid, makeLogicalSessionIdWithTxnUUIDForTest(parentLsid)); +} + +TEST_F(SessionCatalogTest, KillSessionWhenChildSessionIsNotCheckedOut) { + auto runTest = [&](const LogicalSessionId& parentLsid, const LogicalSessionId& childLsid) { + auto killToken = [this, &parentLsid, &childLsid] { + // Create the session so there is something to kill + { + auto opCtx = makeOperationContext(); + opCtx->setLogicalSessionId(childLsid); + OperationContextSession ocs(opCtx.get()); + } + + auto killToken = catalog()->killSession(childLsid); + + // Make sure that the checkOutForKill call will wait for the owning operation context to + // check the session back in + assertConcurrentCheckoutTimesOut(parentLsid); + assertConcurrentCheckoutTimesOut(childLsid); + + return killToken; + }(); + + // Make sure that regular session check-out will fail because the session is marked as + // killed + assertSessionCheckoutTimesOut(childLsid); + + // Check that checking out the parent session will fail because it was marked as killed. + assertSessionCheckoutTimesOut(parentLsid); + + // Schedule a separate "regular operation" thread, which will block on checking-out the + // session, which we will use to confirm that session kill completion actually unblocks + // check-out + auto childFuture = stdx::async(stdx::launch::async, [this, childLsid] { + ThreadClient tc(getServiceContext()); + auto sideOpCtx = Client::getCurrent()->makeOperationContext(); + sideOpCtx->setLogicalSessionId(childLsid); + OperationContextSession ocs(sideOpCtx.get()); + }); + ASSERT(stdx::future_status::ready != + childFuture.wait_for(Milliseconds(10).toSystemDuration())); + + auto parentFuture = stdx::async(stdx::launch::async, [this, parentLsid] { + ThreadClient tc(getServiceContext()); + auto sideOpCtx = Client::getCurrent()->makeOperationContext(); + sideOpCtx->setLogicalSessionId(parentLsid); + OperationContextSession ocs(sideOpCtx.get()); + }); + ASSERT(stdx::future_status::ready != + parentFuture.wait_for(Milliseconds(10).toSystemDuration())); + + // Make sure that "for kill" session check-out succeeds + { + auto opCtx = makeOperationContext(); + auto scopedSession = + catalog()->checkOutSessionForKill(opCtx.get(), std::move(killToken)); + ASSERT_EQ(opCtx.get(), scopedSession.currentOperation_forTest()); + } + + // Make sure that session check-out after kill succeeds again + assertCanCheckoutSession(childLsid); + assertCanCheckoutSession(parentLsid); + + // Make sure the "regular operations" eventually proceed and use the just killed session + childFuture.get(); + parentFuture.get(); + }; + + auto parentLsid = makeLogicalSessionIdForTest(); + runTest(parentLsid, makeLogicalSessionIdWithTxnNumberAndUUIDForTest(parentLsid)); + runTest(parentLsid, makeLogicalSessionIdWithTxnUUIDForTest(parentLsid)); +} + +TEST_F(SessionCatalogTest, KillingChildSessionDoesNotInterruptParentSession) { + auto runTest = [&](const LogicalSessionId& parentLsid, const LogicalSessionId& childLsid) { + auto killToken = [this, &parentLsid, &childLsid] { + assertCanCheckoutSession(childLsid); + + auto opCtx = makeOperationContext(); + opCtx->setLogicalSessionId(parentLsid); + OperationContextSession operationContextSession(opCtx.get()); + + auto killToken = catalog()->killSession(childLsid); + + // Make sure the owning operation context is not interrupted. + opCtx->checkForInterrupt(); + + // Make sure that the checkOutForKill call will wait for the owning operation context to + // check the session back in + assertConcurrentCheckoutTimesOut(parentLsid); + assertConcurrentCheckoutTimesOut(childLsid); + + return killToken; + }(); + + // Use up the killToken. + auto opCtx = makeOperationContext(); + auto scopedSession = catalog()->checkOutSessionForKill(opCtx.get(), std::move(killToken)); + ASSERT_EQ(opCtx.get(), scopedSession.currentOperation_forTest()); + }; + + auto parentLsid = makeLogicalSessionIdForTest(); + runTest(parentLsid, makeLogicalSessionIdWithTxnNumberAndUUIDForTest(parentLsid)); + runTest(parentLsid, makeLogicalSessionIdWithTxnUUIDForTest(parentLsid)); +} + +TEST_F(SessionCatalogTest, KillParentThenChildSession) { + auto runTest = [&](const LogicalSessionId& parentLsid, const LogicalSessionId& childLsid) { + // Create a child session, which will also make the parent session. + assertCanCheckoutSession(childLsid); + + // Kill the parent. + auto parentKillToken = catalog()->killSession(parentLsid); + + // Verify we can't check out either child or parent now. + assertSessionCheckoutTimesOut(childLsid); + assertSessionCheckoutTimesOut(parentLsid); + + // We can still kill the child. + auto childKillToken = catalog()->killSession(childLsid); + + // Verify we can't check back out either session until all kill tokens have been used. + assertSessionCheckoutTimesOut(childLsid); + assertSessionCheckoutTimesOut(parentLsid); + + { + auto opCtx = makeOperationContext(); + auto scopedSession = + catalog()->checkOutSessionForKill(opCtx.get(), std::move(parentKillToken)); + ASSERT_EQ(opCtx.get(), scopedSession.currentOperation_forTest()); + } + assertSessionCheckoutTimesOut(childLsid); + assertSessionCheckoutTimesOut(parentLsid); + + { + auto opCtx = makeOperationContext(); + auto scopedSession = + catalog()->checkOutSessionForKill(opCtx.get(), std::move(childKillToken)); + ASSERT_EQ(opCtx.get(), scopedSession.currentOperation_forTest()); + } + assertCanCheckoutSession(childLsid); + assertCanCheckoutSession(parentLsid); + }; + + auto parentLsid = makeLogicalSessionIdForTest(); + runTest(parentLsid, makeLogicalSessionIdWithTxnNumberAndUUIDForTest(parentLsid)); + runTest(parentLsid, makeLogicalSessionIdWithTxnUUIDForTest(parentLsid)); +} + +TEST_F(SessionCatalogTest, KillChildThenParentSession) { + auto runTest = [&](const LogicalSessionId& parentLsid, const LogicalSessionId& childLsid) { + // Create a child session, which will also make the parent session. + assertCanCheckoutSession(childLsid); + + // Kill the child. + auto childKillToken = catalog()->killSession(childLsid); + + // Verify we can't check out either child or parent now. + assertSessionCheckoutTimesOut(childLsid); + assertSessionCheckoutTimesOut(parentLsid); + + // We can still kill the parent. + auto parentKillToken = catalog()->killSession(parentLsid); + + // Verify we can't check back out either session until all kill tokens have been used. + assertSessionCheckoutTimesOut(childLsid); + assertSessionCheckoutTimesOut(parentLsid); + + { + auto opCtx = makeOperationContext(); + auto scopedSession = + catalog()->checkOutSessionForKill(opCtx.get(), std::move(childKillToken)); + ASSERT_EQ(opCtx.get(), scopedSession.currentOperation_forTest()); + } + assertSessionCheckoutTimesOut(childLsid); + assertSessionCheckoutTimesOut(parentLsid); + + { + auto opCtx = makeOperationContext(); + auto scopedSession = + catalog()->checkOutSessionForKill(opCtx.get(), std::move(parentKillToken)); + ASSERT_EQ(opCtx.get(), scopedSession.currentOperation_forTest()); + } + assertCanCheckoutSession(childLsid); + assertCanCheckoutSession(parentLsid); + }; + + auto parentLsid = makeLogicalSessionIdForTest(); + runTest(parentLsid, makeLogicalSessionIdWithTxnNumberAndUUIDForTest(parentLsid)); + runTest(parentLsid, makeLogicalSessionIdWithTxnUUIDForTest(parentLsid)); +} + +TEST_F(SessionCatalogTest, KillChildAndParentMultipleTimes) { + auto runTest = [&](const LogicalSessionId& parentLsid, const LogicalSessionId& childLsid) { + // Create a child session, which will also make the parent session. + assertCanCheckoutSession(childLsid); + + // Kill the parent and child in interleaved orders to verify the kill order doesn't matter. + auto firstParentKillToken = catalog()->killSession(parentLsid); + + auto firstChildKillToken = catalog()->killSession(childLsid); + auto secondChildKillToken = catalog()->killSession(childLsid); + + auto secondParentKillToken = catalog()->killSession(parentLsid); + + auto thirdChildKillToken = catalog()->killSession(childLsid); + + // Verify we can't check back out either session until all kill tokens have been used. Use + // an arbitrary order to verify the return order doesn't matter. + assertSessionCheckoutTimesOut(childLsid); + assertSessionCheckoutTimesOut(parentLsid); + + { + auto opCtx = makeOperationContext(); + auto scopedSession = + catalog()->checkOutSessionForKill(opCtx.get(), std::move(firstChildKillToken)); + ASSERT_EQ(opCtx.get(), scopedSession.currentOperation_forTest()); + } + assertSessionCheckoutTimesOut(childLsid); + assertSessionCheckoutTimesOut(parentLsid); + + { + auto opCtx = makeOperationContext(); + auto scopedSession = + catalog()->checkOutSessionForKill(opCtx.get(), std::move(secondParentKillToken)); + ASSERT_EQ(opCtx.get(), scopedSession.currentOperation_forTest()); + } + assertSessionCheckoutTimesOut(childLsid); + assertSessionCheckoutTimesOut(parentLsid); + + { + auto opCtx = makeOperationContext(); + auto scopedSession = + catalog()->checkOutSessionForKill(opCtx.get(), std::move(thirdChildKillToken)); + ASSERT_EQ(opCtx.get(), scopedSession.currentOperation_forTest()); + } + assertSessionCheckoutTimesOut(childLsid); + assertSessionCheckoutTimesOut(parentLsid); + + { + auto opCtx = makeOperationContext(); + auto scopedSession = + catalog()->checkOutSessionForKill(opCtx.get(), std::move(firstParentKillToken)); + ASSERT_EQ(opCtx.get(), scopedSession.currentOperation_forTest()); + } + assertSessionCheckoutTimesOut(childLsid); + assertSessionCheckoutTimesOut(parentLsid); + + { + auto opCtx = makeOperationContext(); + auto scopedSession = + catalog()->checkOutSessionForKill(opCtx.get(), std::move(secondChildKillToken)); + ASSERT_EQ(opCtx.get(), scopedSession.currentOperation_forTest()); + } + assertCanCheckoutSession(childLsid); + assertCanCheckoutSession(parentLsid); + }; + + auto parentLsid = makeLogicalSessionIdForTest(); + runTest(parentLsid, makeLogicalSessionIdWithTxnNumberAndUUIDForTest(parentLsid)); + runTest(parentLsid, makeLogicalSessionIdWithTxnUUIDForTest(parentLsid)); +} + + +TEST_F(SessionCatalogTest, MarkSessionAsKilledCanBeCalledMoreThanOnce) { + auto runTest = [&](const LogicalSessionId& lsid) { + // Create the session so there is something to kill + { + auto opCtx = makeOperationContext(); + opCtx->setLogicalSessionId(lsid); + OperationContextSession ocs(opCtx.get()); + } + + auto killToken1 = catalog()->killSession(lsid); + auto killToken2 = catalog()->killSession(lsid); + + // Make sure that regular session check-out will fail because there are two killers on the + // session + assertSessionCheckoutTimesOut(lsid); + + boost::optional<SessionCatalog::KillToken> killTokenWhileSessionIsCheckedOutForKill; + + // Finish the first killer of the session + { + auto opCtx = makeOperationContext(); + auto scopedSession = + catalog()->checkOutSessionForKill(opCtx.get(), std::move(killToken1)); + ASSERT_EQ(opCtx.get(), scopedSession.currentOperation_forTest()); + + // Killing a session while checked out for kill should not affect the killers + killTokenWhileSessionIsCheckedOutForKill.emplace(catalog()->killSession(lsid)); + } + + // Regular session check-out should still fail because there are now still two killers on + // the session + assertSessionCheckoutTimesOut(lsid); + { + auto opCtx = makeOperationContext(); + auto scopedSession = + catalog()->checkOutSessionForKill(opCtx.get(), std::move(killToken2)); + ASSERT_EQ(opCtx.get(), scopedSession.currentOperation_forTest()); + } + { + auto opCtx = makeOperationContext(); + auto scopedSession = catalog()->checkOutSessionForKill( + opCtx.get(), std::move(*killTokenWhileSessionIsCheckedOutForKill)); + ASSERT_EQ(opCtx.get(), scopedSession.currentOperation_forTest()); + } + }; + + runTest(makeLogicalSessionIdForTest()); + runTest(makeLogicalSessionIdWithTxnNumberAndUUIDForTest()); + runTest(makeLogicalSessionIdWithTxnUUIDForTest()); +} + +TEST_F(SessionCatalogTest, MarkNonExistentSessionAsKilled) { + auto runTest = [&](const LogicalSessionId& nonExistentLsid) { + ASSERT_THROWS_CODE( + catalog()->killSession(nonExistentLsid), AssertionException, ErrorCodes::NoSuchSession); + }; + + runTest(makeLogicalSessionIdForTest()); + runTest(makeLogicalSessionIdWithTxnNumberAndUUIDForTest()); + runTest(makeLogicalSessionIdWithTxnUUIDForTest()); +} + +TEST_F(SessionCatalogTest, MarkNonExistentChildSessionAsKilledWhenParentSessionExists) { + auto runTest = [&](const LogicalSessionId& parentLsid, + const LogicalSessionId& nonExistentChildLsid) { + createSession(parentLsid); + ASSERT_THROWS_CODE(catalog()->killSession(nonExistentChildLsid), + AssertionException, + ErrorCodes::NoSuchSession); + }; + + { + auto parentLsid = makeLogicalSessionIdForTest(); + runTest(parentLsid, makeLogicalSessionIdWithTxnNumberAndUUIDForTest(parentLsid)); + } + + { + auto parentLsid = makeLogicalSessionIdForTest(); + runTest(parentLsid, makeLogicalSessionIdWithTxnUUIDForTest(parentLsid)); + } +} + + +TEST_F(SessionCatalogTest, MarkNonExistentChildSessionAsKilledWhenOtherChildSessionExists) { + auto runTest = [&](const LogicalSessionId& existentChildLsid, + const LogicalSessionId& nonExistentChildLsid) { + createSession(existentChildLsid); + ASSERT_THROWS_CODE(catalog()->killSession(nonExistentChildLsid), + AssertionException, + ErrorCodes::NoSuchSession); + }; + + { + auto parentLsid = makeLogicalSessionIdForTest(); + runTest(makeLogicalSessionIdWithTxnNumberAndUUIDForTest(parentLsid), + makeLogicalSessionIdWithTxnNumberAndUUIDForTest(parentLsid)); + } + + { + auto parentLsid = makeLogicalSessionIdForTest(); + runTest(makeLogicalSessionIdWithTxnUUIDForTest(parentLsid), + makeLogicalSessionIdWithTxnUUIDForTest(parentLsid)); + } + + { + auto parentLsid = makeLogicalSessionIdForTest(); + runTest(makeLogicalSessionIdWithTxnNumberAndUUIDForTest(parentLsid), + makeLogicalSessionIdWithTxnUUIDForTest(parentLsid)); + } + + { + auto parentLsid = makeLogicalSessionIdForTest(); + runTest(makeLogicalSessionIdWithTxnUUIDForTest(parentLsid), + makeLogicalSessionIdWithTxnNumberAndUUIDForTest(parentLsid)); + } +} + +TEST_F(SessionCatalogTestWithDefaultOpCtx, SessionDiscarOperationContextAfterCheckIn) { + auto runTest = [&](const LogicalSessionId& lsid) { + _opCtx->setLogicalSessionId(lsid); + + { + OperationContextSession ocs(_opCtx); + ASSERT(OperationContextSession::get(_opCtx)); + + OperationContextSession::checkIn(_opCtx, OperationContextSession::CheckInReason::kDone); + ASSERT(!OperationContextSession::get(_opCtx)); + } + + ASSERT(!OperationContextSession::get(_opCtx)); + }; + + runTest(makeLogicalSessionIdForTest()); + runTest(makeLogicalSessionIdWithTxnNumberAndUUIDForTest()); + runTest(makeLogicalSessionIdWithTxnUUIDForTest()); +} + +TEST_F(SessionCatalogTestWithDefaultOpCtx, SessionDiscarOperationContextAfterCheckInCheckOut) { + auto runTest = [&](const LogicalSessionId& lsid) { + _opCtx->setLogicalSessionId(lsid); + + { + OperationContextSession ocs(_opCtx); + ASSERT(OperationContextSession::get(_opCtx)); + + OperationContextSession::checkIn(_opCtx, OperationContextSession::CheckInReason::kDone); + ASSERT(!OperationContextSession::get(_opCtx)); + + OperationContextSession::checkOut(_opCtx); + ASSERT(OperationContextSession::get(_opCtx)); + } + + ASSERT(!OperationContextSession::get(_opCtx)); + }; + + runTest(makeLogicalSessionIdForTest()); + runTest(makeLogicalSessionIdWithTxnNumberAndUUIDForTest()); + runTest(makeLogicalSessionIdWithTxnUUIDForTest()); +} + +TEST_F(SessionCatalogTestWithDefaultOpCtx, KillSessionsThroughScanSessions) { + // Create sessions in the catalog. + const auto lsids = []() -> std::vector<LogicalSessionId> { + auto lsid0 = makeLogicalSessionIdForTest(); + auto lsid1 = makeLogicalSessionIdWithTxnNumberAndUUIDForTest(); + auto lsid2 = makeLogicalSessionIdWithTxnNumberAndUUIDForTest(); + return {lsid0, lsid1, lsid2}; + }(); + + std::vector<stdx::future<void>> futures; + unittest::Barrier firstUseOfTheSessionReachedBarrier(lsids.size() + 1); + + for (const auto& lsid : lsids) { + futures.emplace_back( + stdx::async(stdx::launch::async, [this, lsid, &firstUseOfTheSessionReachedBarrier] { + ThreadClient tc(getServiceContext()); + + { + auto sideOpCtx = Client::getCurrent()->makeOperationContext(); + sideOpCtx->setLogicalSessionId(lsid); + OperationContextSession ocs(sideOpCtx.get()); + + firstUseOfTheSessionReachedBarrier.countDownAndWait(); + + ASSERT_THROWS_CODE(sideOpCtx->sleepFor(Hours{6}), + AssertionException, + ErrorCodes::ExceededTimeLimit); + } + + { + auto sideOpCtx = Client::getCurrent()->makeOperationContext(); + sideOpCtx->setLogicalSessionId(lsid); + OperationContextSession ocs(sideOpCtx.get()); + } + })); + } + + // Make sure all spawned threads have created the session + firstUseOfTheSessionReachedBarrier.countDownAndWait(); + + // Kill the first and the third sessions + { + std::vector<SessionCatalog::KillToken> firstAndThirdTokens; + catalog()->scanSessions( + SessionKiller::Matcher( + KillAllSessionsByPatternSet{makeKillAllSessionsByPattern(_opCtx)}), + [&lsids, &firstAndThirdTokens](const ObservableSession& session) { + if (session.getSessionId() == lsids[0] || session.getSessionId() == lsids[2]) + firstAndThirdTokens.emplace_back(session.kill(ErrorCodes::ExceededTimeLimit)); + }); + ASSERT_EQ(2U, firstAndThirdTokens.size()); + for (auto& killToken : firstAndThirdTokens) { + auto unusedSheckedOutSessionForKill( + catalog()->checkOutSessionForKill(_opCtx, std::move(killToken))); + } + futures[0].get(); + futures[2].get(); + } + + // Kill the second session + { + std::vector<SessionCatalog::KillToken> secondToken; + catalog()->scanSessions( + SessionKiller::Matcher( + KillAllSessionsByPatternSet{makeKillAllSessionsByPattern(_opCtx)}), + [&lsids, &secondToken](const ObservableSession& session) { + if (session.getSessionId() == lsids[1]) + secondToken.emplace_back(session.kill(ErrorCodes::ExceededTimeLimit)); + }); + ASSERT_EQ(1U, secondToken.size()); + for (auto& killToken : secondToken) { + auto unusedSheckedOutSessionForKill( + catalog()->checkOutSessionForKill(_opCtx, std::move(killToken))); + } + futures[1].get(); + } +} + +// Test that session kill will block normal sesion chechout and will be signaled correctly. +// Even if the implementaion has a bug, the test may not always fail depending on thread +// scheduling, however, this test case still gives us a good coverage. +TEST_F(SessionCatalogTestWithDefaultOpCtx, ConcurrentCheckOutAndKill) { + auto runTest = [&](const LogicalSessionId& lsid) { + auto client = getServiceContext()->makeClient("ConcurrentCheckOutAndKill"); + AlternativeClientRegion acr(client); + auto opCtx = cc().makeOperationContext(); + opCtx->setLogicalSessionId(lsid); + + stdx::future<void> normalCheckOutFinish, killCheckOutFinish; + + // This variable is protected by the session check-out. + std::string lastSessionCheckOut = "first session"; + { + // Check out the session to block both normal check-out and checkOutForKill. + OperationContextSession firstCheckOut(opCtx.get()); + + // Normal check out should start after kill. + normalCheckOutFinish = stdx::async(stdx::launch::async, [&] { + ThreadClient tc(getServiceContext()); + auto sideOpCtx = Client::getCurrent()->makeOperationContext(); + sideOpCtx->setLogicalSessionId(lsid); + OperationContextSession normalCheckOut(sideOpCtx.get()); + ASSERT_EQ("session kill", lastSessionCheckOut); + lastSessionCheckOut = "session checkout"; + }); + + // Kill will short-cut the queue and be the next one to check out. + killCheckOutFinish = stdx::async(stdx::launch::async, [&] { + ThreadClient tc(getServiceContext()); + auto sideOpCtx = Client::getCurrent()->makeOperationContext(); + sideOpCtx->setLogicalSessionId(lsid); + + // Kill the session + std::vector<SessionCatalog::KillToken> killTokens; + catalog()->scanSession(lsid, [&killTokens](const ObservableSession& session) { + killTokens.emplace_back(session.kill(ErrorCodes::InternalError)); + }); + + ASSERT_EQ(1U, killTokens.size()); + auto checkOutSessionForKill( + catalog()->checkOutSessionForKill(sideOpCtx.get(), std::move(killTokens[0]))); + + ASSERT_EQ("first session", lastSessionCheckOut); + lastSessionCheckOut = "session kill"; + }); + + // The main thread won't check in the session until it's killed. + { + auto m = MONGO_MAKE_LATCH(); + stdx::condition_variable cond; + stdx::unique_lock<Latch> lock(m); + ASSERT_THROWS_CODE( + opCtx->waitForConditionOrInterrupt(cond, lock, [] { return false; }), + DBException, + ErrorCodes::InternalError); + } + } + normalCheckOutFinish.get(); + killCheckOutFinish.get(); + + ASSERT_EQ("session checkout", lastSessionCheckOut); + }; + + runTest(makeLogicalSessionIdForTest()); + runTest(makeLogicalSessionIdWithTxnNumberAndUUIDForTest()); + runTest(makeLogicalSessionIdWithTxnUUIDForTest()); +} + +} // namespace +} // namespace mongo diff --git a/src/mongo/db/session/session_killer.cpp b/src/mongo/db/session/session_killer.cpp new file mode 100644 index 00000000000..f9effaa3f22 --- /dev/null +++ b/src/mongo/db/session/session_killer.cpp @@ -0,0 +1,234 @@ +/** + * Copyright (C) 2018-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * 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 + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * 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 Server Side 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/session/session_killer.h" + +#include "mongo/db/client.h" +#include "mongo/db/operation_context.h" +#include "mongo/db/service_context.h" +#include "mongo/db/session/logical_session_id_helpers.h" +#include "mongo/util/destructor_guard.h" +#include "mongo/util/scopeguard.h" + +namespace mongo { + +namespace { +const auto getSessionKiller = ServiceContext::declareDecoration<std::shared_ptr<SessionKiller>>(); +} // namespace + +SessionKiller::SessionKiller(ServiceContext* sc, KillFunc killer) + : _killFunc(std::move(killer)), _urbg(std::random_device{}()), _reapResults() { + _thread = stdx::thread([this, sc] { + // This is the background killing thread + + ThreadClient tc("SessionKiller", sc); + + stdx::unique_lock<Latch> lk(_mutex); + + // While we're not in shutdown + while (!_inShutdown) { + // Wait until we're woken up, and should either shutdown, or have something new to reap. + _killerCV.wait(lk, [&] { return _inShutdown || _nextToReap.size(); }); + + // If we're in shutdown we're done + if (_inShutdown) { + return; + } + + // Otherwise make an opctx and head into kill + auto opCtx = cc().makeOperationContext(); + _periodicKill(opCtx.get(), lk); + } + }); +} + +SessionKiller::~SessionKiller() { + DESTRUCTOR_GUARD([&] { + { + stdx::lock_guard<Latch> lk(_mutex); + _inShutdown = true; + } + _killerCV.notify_one(); + _callerCV.notify_all(); + _thread.join(); + }()); +} + +SessionKiller::ReapResult::ReapResult() : result(std::make_shared<boost::optional<Result>>()) {} + +SessionKiller::Matcher::Matcher(KillAllSessionsByPatternSet&& patterns) + : _patterns(std::move(patterns)) { + for (const auto& item : _patterns) { + auto& pattern = item.pattern; + if (pattern.getUid()) { + _uids.emplace(pattern.getUid().value(), &pattern); + } else if (pattern.getLsid()) { + _lsids.emplace(pattern.getLsid().value(), &pattern); + } else { + // If we're killing everything, it's the only pattern we care about. + decltype(_patterns) onlyKillAll{{item}}; + using std::swap; + swap(_patterns, onlyKillAll); + + _killAll = &(_patterns.begin()->pattern); + break; + } + } +} + +const KillAllSessionsByPatternSet& SessionKiller::Matcher::getPatterns() const { + return _patterns; +} + +const KillAllSessionsByPattern* SessionKiller::Matcher::match(const LogicalSessionId& lsid) const { + if (_killAll) { + return _killAll; + } + + // Since child and parent sessions are logically related, by default, we will convert any child + // lsid to its corresponding parent lsid and match based on the converted lsid. + auto parentSessionId = castToParentSessionId(lsid); + + { + auto iter = _lsids.find(parentSessionId); + if (iter != _lsids.end()) { + return iter->second; + } + } + + { + auto iter = _uids.find(parentSessionId.getUid()); + if (iter != _uids.end()) { + return iter->second; + } + } + + return nullptr; +} + +SessionKiller* SessionKiller::get(ServiceContext* service) { + return getSessionKiller(service).get(); +} + +SessionKiller* SessionKiller::get(OperationContext* ctx) { + return get(ctx->getServiceContext()); +} + +std::shared_ptr<SessionKiller::Result> SessionKiller::kill( + OperationContext* opCtx, const KillAllSessionsByPatternSet& toKill) { + stdx::unique_lock<Latch> lk(_mutex); + + // Save a shared_ptr to the current reapResults (I.e. the next thing to get killed). + auto reapResults = _reapResults; + + // Dump all your lsids in. + for (const auto& item : toKill) { + _nextToReap.emplace(item); + } + + // Wake up the killer. + _killerCV.notify_one(); + + // Wait until our results are there, or the killer is shutting down. + opCtx->waitForConditionOrInterrupt( + _callerCV, lk, [&] { return reapResults.result->is_initialized() || _inShutdown; }); + + // If the killer is shutting down, throw. + uassert(ErrorCodes::ShutdownInProgress, "SessionKiller shutting down", !_inShutdown); + + // Otherwise, alias (via the aliasing ctor of shared_ptr) a shared_ptr to the actual results + // (inside the optional) to keep our contract. That ctor form returns a shared_ptr which + // returns one type, while keeping a refcount on a control block from a different type. + return {reapResults.result, reapResults.result->get_ptr()}; +} + +void SessionKiller::_periodicKill(OperationContext* opCtx, stdx::unique_lock<Latch>& lk) { + // Pull our current workload onto the stack. Swap it for empties. + decltype(_nextToReap) nextToReap; + decltype(_reapResults) reapResults; + { + using std::swap; + swap(nextToReap, _nextToReap); + swap(reapResults, _reapResults); + } + + // Drop the lock and run the killer. + lk.unlock(); + + // Group patterns with equal API parameters into sets. + stdx::unordered_map<APIParameters, KillAllSessionsByPatternSet, APIParameters::Hash> sets; + for (auto& item : nextToReap) { + sets[item.apiParameters].insert(std::move(item)); + } + + // Use the API parameters included in each KillAllSessionsByPattern struct. + IgnoreAPIParametersBlock ignoreApiParametersBlock(opCtx); + Result finalResults{std::vector<HostAndPort>{}}; + for (auto& [apiParameters, items] : sets) { + APIParameters::get(opCtx) = apiParameters; + Matcher matcher(std::move(items)); + boost::optional<Result> results; + try { + results.emplace(_killFunc(opCtx, matcher, &_urbg)); + } catch (...) { + results.emplace(exceptionToStatus()); + } + invariant(results); + if (!results->isOK()) { + finalResults = *results; + break; + } + + finalResults.getValue().insert(finalResults.getValue().end(), + std::make_move_iterator(results->getValue().begin()), + std::make_move_iterator(results->getValue().end())); + } + + // Expose the results and notify callers + lk.lock(); + *(reapResults.result) = std::move(finalResults); + _callerCV.notify_all(); +}; + +void SessionKiller::set(ServiceContext* sc, std::shared_ptr<SessionKiller> sk) { + getSessionKiller(sc) = sk; +} + + +void SessionKiller::shutdown(ServiceContext* sc) { + auto shared = getSessionKiller(sc); + getSessionKiller(sc).reset(); + + // Nuke + shared.reset(); +} + +} // namespace mongo diff --git a/src/mongo/db/session/session_killer.h b/src/mongo/db/session/session_killer.h new file mode 100644 index 00000000000..59e27dc7fc7 --- /dev/null +++ b/src/mongo/db/session/session_killer.h @@ -0,0 +1,153 @@ +/** + * Copyright (C) 2018-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * 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 + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * 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 Server Side 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 <functional> +#include <memory> +#include <random> +#include <vector> + +#include "mongo/base/status_with.h" +#include "mongo/db/session/kill_sessions.h" +#include "mongo/platform/mutex.h" +#include "mongo/stdx/condition_variable.h" +#include "mongo/stdx/thread.h" +#include "mongo/stdx/unordered_set.h" +#include "mongo/util/hierarchical_acquisition.h" +#include "mongo/util/net/hostandport.h" + +namespace mongo { + +/** + * The SessionKiller enforces a single thread for session killing for any given ServiceContext. + * + * The killer owns a background thread which actually does the work, and callers batch their kills + * together each round, before the killer starts its dispatching, after which they batch up for the + * next round. + * + * The KillFunc's kill function is passed in to its constructor, and parameterizes its behavior + * depending on context (mongod/mongos). + */ +class SessionKiller { +public: + /** + * The Result of a call is either: + * + * Status::OK(), empty vector - we killed everything + * Status::OK(), filled vector - we killed something. HostAndPort is filled with nodes we + * failed to kill on. + * + * !Status::OK() - The kill function itself failed. I.e. we may have killed nothing. + * + * This contract has a helper in kill_sessions_common.h which adapts results for command + * implementations. (killSessionsCmdHelper) + */ + using Result = StatusWith<std::vector<HostAndPort>>; + using UniformRandomBitGenerator = std::minstd_rand; + + /** + * For regular sessions, the Matcher will directly match an lsid pattern. For internal sessions, + * the Matcher will return matches on the child sessions of a parent session as well. + */ + class Matcher { + public: + Matcher(KillAllSessionsByPatternSet&& patterns); + + const KillAllSessionsByPatternSet& getPatterns() const; + + const KillAllSessionsByPattern* match(const LogicalSessionId& lsid) const; + + private: + KillAllSessionsByPatternSet _patterns; + LogicalSessionIdMap<const KillAllSessionsByPattern*> _lsids; + stdx::unordered_map<SHA256Block, const KillAllSessionsByPattern*, SHA256Block::Hash> _uids; + const KillAllSessionsByPattern* _killAll = nullptr; + }; + + /** + * A process specific kill function (we have a different impl in mongos versus mongod). + */ + using KillFunc = + std::function<Result(OperationContext*, const Matcher&, UniformRandomBitGenerator* urbg)>; + + /** + * The killer lives as a decoration on the service context. + */ + static SessionKiller* get(ServiceContext* service); + static SessionKiller* get(OperationContext* opCtx); + + /** + * This method binds the SessionKiller to the ServiceContext. + */ + static void set(ServiceContext* ctx, std::shared_ptr<SessionKiller> sk); + + static void shutdown(ServiceContext* ctx); + + explicit SessionKiller(ServiceContext* sc, KillFunc killer); + ~SessionKiller(); + + /** + * This is the api for killSessions commands to invoke the killer. It blocks until the kill is + * finished, or until it fails (times out on all nodes in mongos). + */ + std::shared_ptr<Result> kill(OperationContext* opCtx, + const KillAllSessionsByPatternSet& toKill); + +private: + /** + * This struct is a helper to default fill in the result, which is otherwise a little error + * prone. + */ + struct ReapResult { + ReapResult(); + + std::shared_ptr<boost::optional<Result>> result; + }; + + void _periodicKill(OperationContext* opCtx, stdx::unique_lock<Latch>& lk); + + KillFunc _killFunc; + + stdx::thread _thread; + + Mutex _mutex = MONGO_MAKE_LATCH(HierarchicalAcquisitionLevel(2), "SessionKiller::_mutex"); + stdx::condition_variable _callerCV; + stdx::condition_variable _killerCV; + + UniformRandomBitGenerator _urbg; + + ReapResult _reapResults; + KillAllSessionsByPatternSet _nextToReap; + + bool _inShutdown = false; +}; + +} // namespace mongo diff --git a/src/mongo/db/session/session_txn_record.idl b/src/mongo/db/session/session_txn_record.idl new file mode 100644 index 00000000000..8bf014a16b7 --- /dev/null +++ b/src/mongo/db/session/session_txn_record.idl @@ -0,0 +1,89 @@ +# Copyright (C) 2018-present MongoDB, Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the Server Side Public License, version 1, +# as published by MongoDB, Inc. +# +# 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 +# Server Side Public License for more details. +# +# You should have received a copy of the Server Side Public License +# along with this program. If not, see +# <http://www.mongodb.com/licensing/server-side-public-license>. +# +# 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 Server Side 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. +# + +# Session Transaction Entry IDL File + +global: + cpp_namespace: "mongo" + cpp_includes: + - "mongo/db/session/logical_session_id.h" + - "mongo/db/repl/optime.h" + +imports: + - "mongo/idl/basic_types.idl" + - "mongo/db/session/logical_session_id.idl" + - "mongo/db/repl/replication_types.idl" + +enums: + DurableTxnState: + description: "The state of the most recent durable transaction on a session" + type: string + values: + kPrepared: "prepared" + kCommitted: "committed" + kAborted: "aborted" + kInProgress: "inProgress" + +structs: + sessionTxnRecord: + description: "A document used for storing session transaction states." + strict: false + fields: + _id: + cpp_name: sessionId + type: LogicalSessionId + description: "The id of the session this transaction belongs to." + parentLsid: + cpp_name: parentSessionId + type: LogicalSessionId + description: "If this transaction is a retryable internal transaction, stores the id + of the parent session for the session the transaction belongs to." + optional: true + txnNum: + type: TxnNumber + description: "The id representing this transaction." + txnRetryCounter: + description: "The last seen transaction retry counter for this transaction." + type: TxnRetryCounter + optional: true # Retryable writes do not have a txnRetryCounter field. + startOpTime: + type: optime + description: "The optime of this transaction's first oplog entry." + optional: true + lastWriteOpTime: + type: optime + description: "The optime timestamp component of the last write on this + transaction." + lastWriteDate: + type: date + description: "Wall clock time of the last write which happened on on this + transaction." + state: + type: DurableTxnState + optional: true # Retryable writes do not have a state field. + description: "The state of the most recent durable transaction on the session" diff --git a/src/mongo/db/session/sessions_collection.cpp b/src/mongo/db/session/sessions_collection.cpp new file mode 100644 index 00000000000..4fc79189206 --- /dev/null +++ b/src/mongo/db/session/sessions_collection.cpp @@ -0,0 +1,302 @@ +/** + * Copyright (C) 2018-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * 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 + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * 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 Server Side 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/session/sessions_collection.h" + +#include <functional> +#include <memory> +#include <vector> + +#include "mongo/bson/bsonobjbuilder.h" +#include "mongo/client/dbclient_base.h" +#include "mongo/db/create_indexes_gen.h" +#include "mongo/db/ops/write_ops.h" +#include "mongo/db/repl/read_concern_args.h" +#include "mongo/db/session/logical_session_id.h" +#include "mongo/db/session/logical_session_id_helpers.h" +#include "mongo/rpc/get_status_from_command_result.h" + +namespace mongo { +namespace { + +// This batch size is chosen to ensure that we don't form requests larger than the 16mb limit. +// Especially for refreshes, the updates we send include the full user name (user@db), and user +// names can be quite large (we enforce a max 10k limit for usernames used with sessions). +// +// At 1000 elements, a 16mb payload gives us a budget of 16000 bytes per user, which we should +// comfortably be able to stay under, even with 10k user names. +constexpr size_t kMaxBatchSize = 1000; + +// Used to refresh or remove items from the session collection with write +// concern majority +const WriteConcernOptions kMajorityWriteConcern{WriteConcernOptions::kMajority, + WriteConcernOptions::SyncMode::UNSET, + WriteConcernOptions::kWriteConcernTimeoutSystem}; + + +BSONObj lsidQuery(const LogicalSessionId& lsid) { + return BSON(LogicalSessionRecord::kIdFieldName << lsid.toBSON()); +} + +BSONObj lsidQuery(const LogicalSessionRecord& record) { + return lsidQuery(record.getId()); +} + +BSONArray updateQuery(const LogicalSessionRecord& record) { + // [ { $set : { lastUse : $$NOW } } , { $set : { user: <user> } } ] + + // Build our update doc. + BSONArrayBuilder updateBuilder; + updateBuilder << BSON("$set" << BSON(LogicalSessionRecord::kLastUseFieldName << "$$NOW")); + + if (record.getUser()) { + updateBuilder << BSON("$set" << BSON(LogicalSessionRecord::kUserFieldName + << BSON("name" << *record.getUser()))); + } + + return updateBuilder.arr(); +} + +template <typename TFactory, typename AddLineFn, typename SendFn, typename Container> +void runBulkGeneric(TFactory makeT, AddLineFn addLine, SendFn sendBatch, const Container& items) { + using T = decltype(makeT()); + + size_t i = 0; + boost::optional<T> thing; + + auto setupBatch = [&] { + i = 0; + thing.emplace(makeT()); + }; + + auto sendLocalBatch = [&] { sendBatch(thing.value()); }; + + setupBatch(); + + for (const auto& item : items) { + addLine(*thing, item); + + if (++i >= kMaxBatchSize) { + sendLocalBatch(); + + setupBatch(); + } + } + + if (i > 0) { + sendLocalBatch(); + } +} + +template <typename InitBatchFn, typename AddLineFn, typename SendBatchFn, typename Container> +void runBulkCmd(StringData label, + InitBatchFn&& initBatch, + AddLineFn&& addLine, + SendBatchFn&& sendBatch, + const Container& items) { + BufBuilder buf; + + boost::optional<BSONObjBuilder> batchBuilder; + boost::optional<BSONArrayBuilder> entries; + + auto makeBatch = [&] { + buf.reset(); + batchBuilder.emplace(buf); + initBatch(&(batchBuilder.value())); + entries.emplace(batchBuilder->subarrayStart(label)); + + return &(entries.value()); + }; + + auto sendLocalBatch = [&](BSONArrayBuilder*) { + entries->done(); + sendBatch(batchBuilder->done()); + }; + + runBulkGeneric(makeBatch, addLine, sendLocalBatch, items); +} + +} // namespace + +constexpr StringData SessionsCollection::kSessionsTTLIndex; + +SessionsCollection::SessionsCollection() = default; + +SessionsCollection::~SessionsCollection() = default; + +SessionsCollection::SendBatchFn SessionsCollection::makeSendFnForBatchWrite( + const NamespaceString& ns, DBClientBase* client) { + auto send = [client, ns](BSONObj batch) { + BSONObj res; + if (!client->runCommand(ns.db().toString(), batch, res)) { + uassertStatusOK(getStatusFromCommandResult(res)); + } + }; + + return send; +} + +SessionsCollection::SendBatchFn SessionsCollection::makeSendFnForCommand(const NamespaceString& ns, + DBClientBase* client) { + auto send = [client, ns](BSONObj cmd) { + BSONObj res; + if (!client->runCommand(ns.db().toString(), cmd, res)) { + uassertStatusOK(getStatusFromCommandResult(res)); + } + }; + + return send; +} + +SessionsCollection::FindBatchFn SessionsCollection::makeFindFnForCommand(const NamespaceString& ns, + DBClientBase* client) { + auto send = [client, ns](BSONObj cmd) -> BSONObj { + BSONObj res; + if (!client->runCommand(ns.db().toString(), cmd, res)) { + uassertStatusOK(getStatusFromCommandResult(res)); + } + + return res; + }; + + return send; +} + +void SessionsCollection::_doRefresh(const NamespaceString& ns, + const std::vector<LogicalSessionRecord>& sessions, + SendBatchFn send) { + auto init = [ns](BSONObjBuilder* batch) { + batch->append("update", ns.coll()); + batch->append("ordered", false); + batch->append(WriteConcernOptions::kWriteConcernField, kMajorityWriteConcern.toBSON()); + }; + + auto add = [](BSONArrayBuilder* entries, const LogicalSessionRecord& record) { + entries->append( + BSON("q" << lsidQuery(record) << "u" << updateQuery(record) << "upsert" << true)); + }; + + runBulkCmd("updates", init, add, send, sessions); +} + +void SessionsCollection::_doRemove(const NamespaceString& ns, + const std::vector<LogicalSessionId>& sessions, + SendBatchFn send) { + auto init = [ns](BSONObjBuilder* batch) { + batch->append("delete", ns.coll()); + batch->append("ordered", false); + batch->append(WriteConcernOptions::kWriteConcernField, kMajorityWriteConcern.toBSON()); + }; + + auto add = [](BSONArrayBuilder* builder, const LogicalSessionId& lsid) { + builder->append(BSON("q" << lsidQuery(lsid) << "limit" << 0)); + }; + + runBulkCmd("deletes", init, add, send, sessions); +} + +LogicalSessionIdSet SessionsCollection::_doFindRemoved( + const NamespaceString& ns, const std::vector<LogicalSessionId>& sessions, FindBatchFn send) { + auto makeT = [] { return std::vector<LogicalSessionId>{}; }; + + auto add = [](std::vector<LogicalSessionId>& batch, const LogicalSessionId& record) { + batch.push_back(record); + }; + + LogicalSessionIdSet removed{sessions.begin(), sessions.end()}; + + auto wrappedSend = [&](BSONObj batch) { + BSONObjBuilder batchWithReadConcernLocal(batch); + batchWithReadConcernLocal.append(repl::ReadConcernArgs::kReadConcernFieldName, + repl::ReadConcernArgs::kLocal); + auto swBatchResult = send(batchWithReadConcernLocal.obj()); + + auto result = SessionsCollectionFetchResult::parse( + IDLParserContext{"SessionsCollectionFetchResult"}, swBatchResult); + + for (const auto& lsid : result.getCursor().getFirstBatch()) { + removed.erase(lsid.get_id()); + } + }; + + auto sendLocal = [&](std::vector<LogicalSessionId>& batch) { + SessionsCollectionFetchRequest request; + + request.setFind(ns.coll()); + request.setFilter({}); + request.getFilter().set_id({}); + request.getFilter().get_id().setIn(batch); + + request.setProjection({}); + request.getProjection().set_id(1); + request.setBatchSize(batch.size()); + request.setLimit(batch.size()); + request.setSingleBatch(true); + + wrappedSend(request.toBSON()); + }; + + runBulkGeneric(makeT, add, sendLocal, sessions); + + return removed; +} + +BSONObj SessionsCollection::generateCreateIndexesCmd() { + NewIndexSpec index; + index.setKey(BSON("lastUse" << 1)); + index.setName(kSessionsTTLIndex); + index.setExpireAfterSeconds(localLogicalSessionTimeoutMinutes * 60); + + CreateIndexesCommand createIndexes(NamespaceString::kLogicalSessionsNamespace); + createIndexes.setIndexes({index.toBSON()}); + + return createIndexes.toBSON(BSON(WriteConcernOptions::kWriteConcernField + << WriteConcernOptions::kInternalWriteDefault)); +} + +BSONObj SessionsCollection::generateCollModCmd() { + BSONObjBuilder collModCmdBuilder; + + collModCmdBuilder << "collMod" << NamespaceString::kLogicalSessionsNamespace.coll(); + + BSONObjBuilder indexBuilder(collModCmdBuilder.subobjStart("index")); + indexBuilder << "name" << kSessionsTTLIndex; + indexBuilder << "expireAfterSeconds" << localLogicalSessionTimeoutMinutes * 60; + + indexBuilder.done(); + collModCmdBuilder.append(WriteConcernOptions::kWriteConcernField, + WriteConcernOptions::kInternalWriteDefault); + collModCmdBuilder.done(); + + return collModCmdBuilder.obj(); +} + +} // namespace mongo diff --git a/src/mongo/db/session/sessions_collection.h b/src/mongo/db/session/sessions_collection.h new file mode 100644 index 00000000000..34542c9a682 --- /dev/null +++ b/src/mongo/db/session/sessions_collection.h @@ -0,0 +1,133 @@ +/** + * Copyright (C) 2018-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * 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 + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * 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 Server Side 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 <functional> + +#include "mongo/db/session/logical_session_id.h" + +namespace mongo { + +class DBClientBase; +class OperationContext; + +/** + * An abstract interface describing the entrypoint into the sessions collection. + * + * Different server deployments (standalone, replica set, sharded cluster) should + * implement their own classes that fulfill this interface. + */ +class SessionsCollection { +public: + static constexpr StringData kSessionsTTLIndex = "lsidTTLIndex"_sd; + + virtual ~SessionsCollection(); + + /** + * Ensures that the sessions collection exists and has the proper indexes. Implementations of + * this method must support multiple concurrent invocations. + */ + virtual void setupSessionsCollection(OperationContext* opCtx) = 0; + + /** + * Checks if the sessions collection exists and has the proper indexes. + */ + virtual void checkSessionsCollectionExists(OperationContext* opCtx) = 0; + + /** + * Updates the last-use times on the given sessions to be greater than or equal to the given + * time. Throws an exception if a networking issue occurred. + */ + virtual void refreshSessions(OperationContext* opCtx, + const LogicalSessionRecordSet& sessions) = 0; + + /** + * Removes the authoritative records for the specified sessions. + * + * Implementations should perform authentication checks to ensure that session records may only + * be removed if their owner is logged in. + * + * Throws an exception if the removal fails, for example from a network error. + */ + virtual void removeRecords(OperationContext* opCtx, const LogicalSessionIdSet& sessions) = 0; + + /** + * Checks a set of lsids and returns the set that no longer exists. + * + * Throws an exception if the fetch cannot occur, for example from a network error. + */ + virtual LogicalSessionIdSet findRemovedSessions(OperationContext* opCtx, + const LogicalSessionIdSet& sessions) = 0; + + /** + * Generates a createIndexes command for the sessions collection TTL index. + */ + static BSONObj generateCreateIndexesCmd(); + + /* + * Generates a collMod command for the sessions collection TTL index. + */ + static BSONObj generateCollModCmd(); + +protected: + SessionsCollection(); + + using SendBatchFn = std::function<void(BSONObj batch)>; + static SendBatchFn makeSendFnForCommand(const NamespaceString& ns, DBClientBase* client); + static SendBatchFn makeSendFnForBatchWrite(const NamespaceString& ns, DBClientBase* client); + + using FindBatchFn = std::function<BSONObj(BSONObj batch)>; + static FindBatchFn makeFindFnForCommand(const NamespaceString& ns, DBClientBase* client); + + /** + * Formats and sends batches of refreshes for the given set of sessions. + */ + void _doRefresh(const NamespaceString& ns, + const std::vector<LogicalSessionRecord>& sessions, + SendBatchFn send); + + /** + * Formats and sends batches of deletes for the given set of sessions. + */ + void _doRemove(const NamespaceString& ns, + const std::vector<LogicalSessionId>& sessions, + SendBatchFn send); + + /** + * Returns those lsids from the input 'sessions' array which are not present in the sessions + * collection. Note that a parent session and its child sessions are tracked as one session in + * the sessions collection. + */ + LogicalSessionIdSet _doFindRemoved(const NamespaceString& ns, + const std::vector<LogicalSessionId>& sessions, + FindBatchFn send); +}; + +} // namespace mongo diff --git a/src/mongo/db/session/sessions_collection_mock.cpp b/src/mongo/db/session/sessions_collection_mock.cpp new file mode 100644 index 00000000000..fd00244e477 --- /dev/null +++ b/src/mongo/db/session/sessions_collection_mock.cpp @@ -0,0 +1,113 @@ +/** + * Copyright (C) 2018-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * 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 + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * 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 Server Side 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 <functional> + +#include "mongo/db/session/sessions_collection_mock.h" +#include "mongo/platform/basic.h" + +namespace mongo { + +MockSessionsCollectionImpl::MockSessionsCollectionImpl() + : _refresh([=](const LogicalSessionRecordSet& sessions) { _refreshSessions(sessions); }), + _remove([=](const LogicalSessionIdSet& sessions) { _removeRecords(sessions); }) {} + +void MockSessionsCollectionImpl::setRefreshHook(RefreshHook hook) { + _refresh = std::move(hook); +} + +void MockSessionsCollectionImpl::setRemoveHook(RemoveHook hook) { + _remove = std::move(hook); +} + +void MockSessionsCollectionImpl::clearHooks() { + _refresh = [=](const LogicalSessionRecordSet& sessions) { _refreshSessions(sessions); }; + _remove = [=](const LogicalSessionIdSet& sessions) { _removeRecords(sessions); }; +} + +void MockSessionsCollectionImpl::refreshSessions(const LogicalSessionRecordSet& sessions) { + _refresh(sessions); +} + +void MockSessionsCollectionImpl::removeRecords(const LogicalSessionIdSet& sessions) { + _remove(std::move(sessions)); +} + +void MockSessionsCollectionImpl::add(LogicalSessionRecord record) { + stdx::unique_lock<Latch> lk(_mutex); + _sessions.insert({record.getId(), std::move(record)}); +} + +void MockSessionsCollectionImpl::remove(LogicalSessionId lsid) { + stdx::unique_lock<Latch> lk(_mutex); + _sessions.erase(lsid); +} + +bool MockSessionsCollectionImpl::has(LogicalSessionId lsid) { + stdx::unique_lock<Latch> lk(_mutex); + return _sessions.find(lsid) != _sessions.end(); +} + +void MockSessionsCollectionImpl::clearSessions() { + stdx::unique_lock<Latch> lk(_mutex); + _sessions.clear(); +} + +const MockSessionsCollectionImpl::SessionMap& MockSessionsCollectionImpl::sessions() const { + return _sessions; +} + +void MockSessionsCollectionImpl::_refreshSessions(const LogicalSessionRecordSet& sessions) { + for (auto& record : sessions) { + if (!has(record.getId())) { + _sessions.insert({record.getId(), record}); + } + } +} + +void MockSessionsCollectionImpl::_removeRecords(const LogicalSessionIdSet& sessions) { + stdx::unique_lock<Latch> lk(_mutex); + for (auto& lsid : sessions) { + _sessions.erase(lsid); + } +} + +LogicalSessionIdSet MockSessionsCollectionImpl::findRemovedSessions( + OperationContext* opCtx, const LogicalSessionIdSet& sessions) { + LogicalSessionIdSet lsids; + stdx::unique_lock<Latch> lk(_mutex); + for (auto& lsid : sessions) { + if (_sessions.find(lsid) == _sessions.end()) { + lsids.emplace(lsid); + } + } + return lsids; +} + +} // namespace mongo diff --git a/src/mongo/db/session/sessions_collection_mock.h b/src/mongo/db/session/sessions_collection_mock.h new file mode 100644 index 00000000000..cafa41e05f1 --- /dev/null +++ b/src/mongo/db/session/sessions_collection_mock.h @@ -0,0 +1,131 @@ +/** + * Copyright (C) 2018-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * 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 + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * 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 Server Side 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 <functional> + +#include "mongo/db/session/logical_session_id.h" +#include "mongo/db/session/sessions_collection.h" +#include "mongo/platform/mutex.h" +#include "mongo/stdx/unordered_map.h" + +namespace mongo { + +/** + * To allow us to move a MockSessionCollection into the session cache while + * maintaining a hold on it from within our unit tests, the MockSessionCollection + * will have an internal pointer to a MockSessionsCollectionImpl object that the + * test creates and controls. + * + * This class can operate in two modes: + * + * - if no custom hooks are set, then the test caller may add() and remove() items + * from the _sessions map, and this class will simply perform the required + * operations against that set. + * + * - if custom hooks are set, then the hooks will be run instead of the provided + * defaults, and the internal _sessions map will NOT be updated. + */ +class MockSessionsCollectionImpl { +public: + using SessionMap = + stdx::unordered_map<LogicalSessionId, LogicalSessionRecord, LogicalSessionIdHash>; + + MockSessionsCollectionImpl(); + + using RefreshHook = std::function<void(const LogicalSessionRecordSet&)>; + using RemoveHook = std::function<void(const LogicalSessionIdSet&)>; + + // Set custom hooks to override default behavior + void setRefreshHook(RefreshHook hook); + void setRemoveHook(RemoveHook hook); + + // Reset all hooks to their defaults + void clearHooks(); + + // Forwarding methods from the MockSessionsCollection + void refreshSessions(const LogicalSessionRecordSet& sessions); + void removeRecords(const LogicalSessionIdSet& sessions); + + // Test-side methods that operate on the _sessions map + void add(LogicalSessionRecord record); + void remove(LogicalSessionId lsid); + bool has(LogicalSessionId lsid); + void clearSessions(); + const SessionMap& sessions() const; + + LogicalSessionIdSet findRemovedSessions(OperationContext* opCtx, + const LogicalSessionIdSet& sessions); + +private: + // Default implementations, may be overridden with custom hooks. + void _refreshSessions(const LogicalSessionRecordSet& sessions); + void _removeRecords(const LogicalSessionIdSet& sessions); + + Mutex _mutex = MONGO_MAKE_LATCH("MockSessionsCollectionImpl::_mutex"); + SessionMap _sessions; + + RefreshHook _refresh; + RemoveHook _remove; +}; + +/** + * To allow us to move this into the session cache while maintaining a hold + * on it from the test side, the MockSessionCollection will have an internal pointer + * to an impl that we maintain access to. + */ +class MockSessionsCollection : public SessionsCollection { +public: + explicit MockSessionsCollection(std::shared_ptr<MockSessionsCollectionImpl> impl) + : _impl(std::move(impl)) {} + + void setupSessionsCollection(OperationContext* opCtx) override {} + + void checkSessionsCollectionExists(OperationContext* opCtx) override {} + + void refreshSessions(OperationContext* opCtx, + const LogicalSessionRecordSet& sessions) override { + _impl->refreshSessions(sessions); + } + + void removeRecords(OperationContext* opCtx, const LogicalSessionIdSet& sessions) override { + _impl->removeRecords(sessions); + } + + LogicalSessionIdSet findRemovedSessions(OperationContext* opCtx, + const LogicalSessionIdSet& sessions) override { + return _impl->findRemovedSessions(opCtx, sessions); + } + +private: + std::shared_ptr<MockSessionsCollectionImpl> _impl; +}; + +} // namespace mongo diff --git a/src/mongo/db/session/sessions_collection_rs.cpp b/src/mongo/db/session/sessions_collection_rs.cpp new file mode 100644 index 00000000000..865f0b825e9 --- /dev/null +++ b/src/mongo/db/session/sessions_collection_rs.cpp @@ -0,0 +1,240 @@ +/** + * Copyright (C) 2018-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * 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 + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * 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 Server Side 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/session/sessions_collection_rs.h" + +#include <boost/optional.hpp> +#include <memory> +#include <utility> + +#include "mongo/bson/bsonobj.h" +#include "mongo/client/authenticate.h" +#include "mongo/client/connection_string.h" +#include "mongo/client/read_preference.h" +#include "mongo/client/remote_command_targeter_factory_impl.h" +#include "mongo/db/concurrency/d_concurrency.h" +#include "mongo/db/dbdirectclient.h" +#include "mongo/db/operation_context.h" +#include "mongo/db/repl/repl_set_config.h" +#include "mongo/db/repl/replication_coordinator.h" +#include "mongo/rpc/get_status_from_command_result.h" +#include "mongo/util/assert_util.h" + +namespace mongo { + +auto SessionsCollectionRS::_makePrimaryConnection(OperationContext* opCtx) { + // Find the primary + if (stdx::lock_guard lk(_mutex); !_targeter) { + // There is an assumption here that for the lifetime of a given process, the + // ReplicationCoordiation will only return configs for a single replica set + auto coord = mongo::repl::ReplicationCoordinator::get(opCtx); + auto config = coord->getConfig(); + uassert(ErrorCodes::NotYetInitialized, + "Replication has not yet been configured", + config.isInitialized()); + + RemoteCommandTargeterFactoryImpl factory; + _targeter = factory.create(config.getConnectionString()); + } + + auto res = uassertStatusOK( + _targeter->findHost(opCtx, ReadPreferenceSetting(ReadPreference::PrimaryOnly))); + + auto conn = std::make_unique<ScopedDbConnection>(res.toString()); + + // Make a connection to the primary, auth, then send + if (auth::isInternalAuthSet()) { + uassertStatusOK(conn->get()->authenticateInternalUser()); + } + + return conn; +} + +bool SessionsCollectionRS::_isStandaloneOrPrimary(const NamespaceString& ns, + OperationContext* opCtx) { + ShouldNotConflictWithSecondaryBatchApplicationBlock noPBWMBlock(opCtx->lockState()); + Lock::DBLock lk(opCtx, ns.dbName(), MODE_IS); + Lock::CollectionLock lock(opCtx, NamespaceString::kLogicalSessionsNamespace, MODE_IS); + + auto coord = mongo::repl::ReplicationCoordinator::get(opCtx); + + return coord->canAcceptWritesForDatabase(opCtx, ns.db()); +} + +template <typename LocalCallback, typename RemoteCallback> +auto SessionsCollectionRS::_dispatch(const NamespaceString& ns, + OperationContext* opCtx, + LocalCallback&& localCallback, + RemoteCallback&& remoteCallback) + -> CommonResultT<LocalCallback, RemoteCallback> { + if (_isStandaloneOrPrimary(ns, opCtx)) { + return std::forward<LocalCallback>(localCallback)(); + } + + // There is a window here where we may transition from Primary to Secondary after we release + // the locks we take in _isStandaloneOrPrimary(). In this case, the callback we run below + // may throw a NotWritablePrimary error, or a stale read. However, this is preferable to running + // the callback while we hold locks, since that can lead to a deadlock. + + auto conn = _makePrimaryConnection(opCtx); + DBClientBase* client = conn->get(); + ScopeGuard guard([&] { conn->done(); }); + try { + return std::forward<RemoteCallback>(remoteCallback)(client); + } catch (...) { + guard.dismiss(); + conn->kill(); + throw; + } +} + +void SessionsCollectionRS::setupSessionsCollection(OperationContext* opCtx) { + _dispatch( + NamespaceString::kLogicalSessionsNamespace, + opCtx, + [&] { + try { + checkSessionsCollectionExists(opCtx); + } catch (const DBException& ex) { + + DBDirectClient client(opCtx); + BSONObj cmd; + + if (ex.code() == ErrorCodes::IndexOptionsConflict) { + cmd = generateCollModCmd(); + } else { + // Creating the TTL index will auto-generate the collection. + cmd = generateCreateIndexesCmd(); + } + + BSONObj info; + if (!client.runCommand( + NamespaceString::kLogicalSessionsNamespace.db().toString(), cmd, info)) { + uassertStatusOK(getStatusFromCommandResult(info)); + } + } + }, + [&](DBClientBase*) { checkSessionsCollectionExists(opCtx); }); +} + +void SessionsCollectionRS::checkSessionsCollectionExists(OperationContext* opCtx) { + DBDirectClient client(opCtx); + + const bool includeBuildUUIDs = false; + const int options = 0; + auto indexes = client.getIndexSpecs( + NamespaceString::kLogicalSessionsNamespace, includeBuildUUIDs, options); + + uassert(ErrorCodes::NamespaceNotFound, + str::stream() << NamespaceString::kLogicalSessionsNamespace << " does not exist", + indexes.size() != 0u); + + auto index = std::find_if(indexes.begin(), indexes.end(), [](const BSONObj& index) { + return index.getField("name").String() == kSessionsTTLIndex; + }); + + uassert(ErrorCodes::IndexNotFound, + str::stream() << NamespaceString::kLogicalSessionsNamespace + << " does not have the required TTL index", + index != indexes.end()); + + uassert(ErrorCodes::IndexOptionsConflict, + str::stream() << NamespaceString::kLogicalSessionsNamespace + << " currently has the incorrect timeout for the TTL index", + index->hasField("expireAfterSeconds") && + index->getField("expireAfterSeconds").Int() == + (localLogicalSessionTimeoutMinutes * 60)); +} + +void SessionsCollectionRS::refreshSessions(OperationContext* opCtx, + const LogicalSessionRecordSet& sessions) { + const std::vector<LogicalSessionRecord> sessionsVector(sessions.begin(), sessions.end()); + + _dispatch(NamespaceString::kLogicalSessionsNamespace, + opCtx, + [&] { + DBDirectClient client(opCtx); + _doRefresh( + NamespaceString::kLogicalSessionsNamespace, + sessionsVector, + makeSendFnForBatchWrite(NamespaceString::kLogicalSessionsNamespace, &client)); + }, + [&](DBClientBase* client) { + _doRefresh( + NamespaceString::kLogicalSessionsNamespace, + sessionsVector, + makeSendFnForBatchWrite(NamespaceString::kLogicalSessionsNamespace, client)); + }); +} + +void SessionsCollectionRS::removeRecords(OperationContext* opCtx, + const LogicalSessionIdSet& sessions) { + const std::vector<LogicalSessionId> sessionsVector(sessions.begin(), sessions.end()); + + _dispatch( + NamespaceString::kLogicalSessionsNamespace, + opCtx, + [&] { + DBDirectClient client(opCtx); + _doRemove(NamespaceString::kLogicalSessionsNamespace, + sessionsVector, + makeSendFnForBatchWrite(NamespaceString::kLogicalSessionsNamespace, &client)); + }, + [&](DBClientBase* client) { + _doRemove(NamespaceString::kLogicalSessionsNamespace, + sessionsVector, + makeSendFnForBatchWrite(NamespaceString::kLogicalSessionsNamespace, client)); + }); +} + +LogicalSessionIdSet SessionsCollectionRS::findRemovedSessions(OperationContext* opCtx, + const LogicalSessionIdSet& sessions) { + const std::vector<LogicalSessionId> sessionsVector(sessions.begin(), sessions.end()); + + return _dispatch( + NamespaceString::kLogicalSessionsNamespace, + opCtx, + [&] { + DBDirectClient client(opCtx); + return _doFindRemoved( + NamespaceString::kLogicalSessionsNamespace, + sessionsVector, + makeFindFnForCommand(NamespaceString::kLogicalSessionsNamespace, &client)); + }, + [&](DBClientBase* client) { + return _doFindRemoved( + NamespaceString::kLogicalSessionsNamespace, + sessionsVector, + makeFindFnForCommand(NamespaceString::kLogicalSessionsNamespace, client)); + }); +} + +} // namespace mongo diff --git a/src/mongo/db/session/sessions_collection_rs.h b/src/mongo/db/session/sessions_collection_rs.h new file mode 100644 index 00000000000..d88b32866c6 --- /dev/null +++ b/src/mongo/db/session/sessions_collection_rs.h @@ -0,0 +1,115 @@ +/** + * Copyright (C) 2018-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * 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 + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * 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 Server Side 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 <memory> +#include <type_traits> + +#include "mongo/client/connpool.h" +#include "mongo/client/remote_command_targeter.h" +#include "mongo/db/namespace_string.h" +#include "mongo/db/session/logical_session_id.h" +#include "mongo/db/session/sessions_collection.h" +#include "mongo/platform/mutex.h" +#include "mongo/util/time_support.h" + +namespace mongo { + +class OperationContext; + +/** + * Accesses the sessions collection for replica set members. + */ +class SessionsCollectionRS final : public SessionsCollection { +public: + /** + * Ensures that the sessions collection exists and has the proper indexes. + */ + void setupSessionsCollection(OperationContext* opCtx) override; + + /** + * Checks if the sessions collection exists and has the proper indexes. + */ + void checkSessionsCollectionExists(OperationContext* opCtx) override; + + /** + * Updates the last-use times on the given sessions to be greater than + * or equal to the current time. + * + * If a step-down happens on this node as this method is running, it may fail. + */ + void refreshSessions(OperationContext* opCtx, const LogicalSessionRecordSet& sessions) override; + + /** + * Removes the authoritative records for the specified sessions. + * + * If a step-down happens on this node as this method is running, it may fail. + */ + void removeRecords(OperationContext* opCtx, const LogicalSessionIdSet& sessions) override; + + /** + * Returns the subset of sessions from the given set that do not have entries + * in the sessions collection. + * + * If a step-down happens on this node as this method is running, it may + * return stale results. + */ + LogicalSessionIdSet findRemovedSessions(OperationContext* opCtx, + const LogicalSessionIdSet& sessions) override; + +private: + auto _makePrimaryConnection(OperationContext* opCtx); + + bool _isStandaloneOrPrimary(const NamespaceString& ns, OperationContext* opCtx); + + template <typename LocalCallback, typename RemoteCallback> + struct CommonResult { + using LocalReturnType = std::invoke_result_t<LocalCallback>; + using RemoteReturnType = std::invoke_result_t<RemoteCallback, DBClientBase*>; + static_assert(std::is_same_v<LocalReturnType, RemoteReturnType>, + "LocalCallback and RemoteCallback must have the same return type"); + + using Type = + std::conditional_t<std::is_void<LocalReturnType>::value, void, LocalReturnType>; + }; + template <typename LocalCallback, typename RemoteCallback> + using CommonResultT = typename CommonResult<LocalCallback, RemoteCallback>::Type; + + template <typename LocalCallback, typename RemoteCallback> + CommonResultT<LocalCallback, RemoteCallback> _dispatch(const NamespaceString& ns, + OperationContext* opCtx, + LocalCallback&& localCallback, + RemoteCallback&& remoteCallback); + + Mutex _mutex = MONGO_MAKE_LATCH("SessionsCollectionRS::_mutex"); + std::unique_ptr<RemoteCommandTargeter> _targeter; +}; + +} // namespace mongo diff --git a/src/mongo/db/session/sessions_collection_standalone.cpp b/src/mongo/db/session/sessions_collection_standalone.cpp new file mode 100644 index 00000000000..94fb61cfc75 --- /dev/null +++ b/src/mongo/db/session/sessions_collection_standalone.cpp @@ -0,0 +1,128 @@ +/** + * Copyright (C) 2018-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * 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 + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * 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 Server Side 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/session/sessions_collection_standalone.h" + +#include "mongo/db/dbdirectclient.h" +#include "mongo/db/operation_context.h" +#include "mongo/rpc/get_status_from_command_result.h" + +namespace mongo { + +namespace { + +BSONObj lsidQuery(const LogicalSessionId& lsid) { + return BSON(LogicalSessionRecord::kIdFieldName << lsid.toBSON()); +} +} // namespace + +void SessionsCollectionStandalone::setupSessionsCollection(OperationContext* opCtx) { + try { + checkSessionsCollectionExists(opCtx); + } catch (DBException& ex) { + + DBDirectClient client(opCtx); + BSONObj cmd; + + if (ex.code() == ErrorCodes::IndexOptionsConflict) { + cmd = generateCollModCmd(); + } else { + cmd = generateCreateIndexesCmd(); + } + + BSONObj info; + if (!client.runCommand( + NamespaceString::kLogicalSessionsNamespace.db().toString(), cmd, info)) { + uassertStatusOKWithContext(getStatusFromCommandResult(info), + str::stream() << "Failed to create " + << NamespaceString::kLogicalSessionsNamespace); + } + } +} + +void SessionsCollectionStandalone::checkSessionsCollectionExists(OperationContext* opCtx) { + DBDirectClient client(opCtx); + + const bool includeBuildUUIDs = false; + const int options = 0; + auto indexes = client.getIndexSpecs( + NamespaceString::kLogicalSessionsNamespace, includeBuildUUIDs, options); + + uassert(ErrorCodes::NamespaceNotFound, + str::stream() << NamespaceString::kLogicalSessionsNamespace << " does not exist", + indexes.size() != 0u); + + auto index = std::find_if(indexes.begin(), indexes.end(), [](const BSONObj& index) { + return index.getField("name").String() == kSessionsTTLIndex; + }); + + uassert(ErrorCodes::IndexNotFound, + str::stream() << NamespaceString::kLogicalSessionsNamespace + << " does not have the required TTL index", + index != indexes.end()); + + uassert(ErrorCodes::IndexOptionsConflict, + str::stream() << NamespaceString::kLogicalSessionsNamespace + << " currently has the incorrect timeout for the TTL index", + index->hasField("expireAfterSeconds") && + index->getField("expireAfterSeconds").Int() == + (localLogicalSessionTimeoutMinutes * 60)); +} + +void SessionsCollectionStandalone::refreshSessions(OperationContext* opCtx, + const LogicalSessionRecordSet& sessions) { + const std::vector<LogicalSessionRecord> sessionsVector(sessions.begin(), sessions.end()); + DBDirectClient client(opCtx); + _doRefresh(NamespaceString::kLogicalSessionsNamespace, + sessionsVector, + makeSendFnForBatchWrite(NamespaceString::kLogicalSessionsNamespace, &client)); +} + +void SessionsCollectionStandalone::removeRecords(OperationContext* opCtx, + const LogicalSessionIdSet& sessions) { + const std::vector<LogicalSessionId> sessionsVector(sessions.begin(), sessions.end()); + DBDirectClient client(opCtx); + _doRemove(NamespaceString::kLogicalSessionsNamespace, + sessionsVector, + makeSendFnForBatchWrite(NamespaceString::kLogicalSessionsNamespace, &client)); +} + +LogicalSessionIdSet SessionsCollectionStandalone::findRemovedSessions( + OperationContext* opCtx, const LogicalSessionIdSet& sessions) { + const std::vector<LogicalSessionId> sessionsVector(sessions.begin(), sessions.end()); + DBDirectClient client(opCtx); + return _doFindRemoved( + NamespaceString::kLogicalSessionsNamespace, + sessionsVector, + makeFindFnForCommand(NamespaceString::kLogicalSessionsNamespace, &client)); +} + +} // namespace mongo diff --git a/src/mongo/db/session/sessions_collection_standalone.h b/src/mongo/db/session/sessions_collection_standalone.h new file mode 100644 index 00000000000..f0f3d567d20 --- /dev/null +++ b/src/mongo/db/session/sessions_collection_standalone.h @@ -0,0 +1,71 @@ +/** + * Copyright (C) 2018-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * 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 + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * 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 Server Side 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 "mongo/db/session/logical_session_id.h" +#include "mongo/db/session/sessions_collection.h" +#include "mongo/util/time_support.h" + +namespace mongo { + +class DBDirectClient; +class OperationContext; + +/** + * Accesses the sessions collection directly for standalone servers. + */ +class SessionsCollectionStandalone : public SessionsCollection { +public: + /** + * Ensures that the sessions collection exists and has the proper indexes. + */ + void setupSessionsCollection(OperationContext* opCtx) override; + + /** + * Checks if the sessions collection exists and has the proper indexes. + */ + void checkSessionsCollectionExists(OperationContext* opCtx) override; + + /** + * Updates the last-use times on the given sessions to be greater than + * or equal to the current time. + */ + void refreshSessions(OperationContext* opCtx, const LogicalSessionRecordSet& sessions) override; + + /** + * Removes the authoritative records for the specified sessions. + */ + void removeRecords(OperationContext* opCtx, const LogicalSessionIdSet& sessions) override; + + LogicalSessionIdSet findRemovedSessions(OperationContext* opCtx, + const LogicalSessionIdSet& sessions) override; +}; + +} // namespace mongo |
