summaryrefslogtreecommitdiff
path: root/src/mongo/db/session
diff options
context:
space:
mode:
Diffstat (limited to 'src/mongo/db/session')
-rw-r--r--src/mongo/db/session/SConscript179
-rw-r--r--src/mongo/db/session/initialize_operation_session_info.cpp173
-rw-r--r--src/mongo/db/session/initialize_operation_session_info.h58
-rw-r--r--src/mongo/db/session/internal_session_pool.cpp198
-rw-r--r--src/mongo/db/session/internal_session_pool.h120
-rw-r--r--src/mongo/db/session/internal_session_pool_test.cpp282
-rw-r--r--src/mongo/db/session/kill_sessions.cpp141
-rw-r--r--src/mongo/db/session/kill_sessions.h118
-rw-r--r--src/mongo/db/session/kill_sessions.idl98
-rw-r--r--src/mongo/db/session/kill_sessions_common.cpp126
-rw-r--r--src/mongo/db/session/kill_sessions_common.h169
-rw-r--r--src/mongo/db/session/kill_sessions_local.cpp249
-rw-r--r--src/mongo/db/session/kill_sessions_local.h80
-rw-r--r--src/mongo/db/session/kill_sessions_util.h51
-rw-r--r--src/mongo/db/session/logical_session_cache.cpp67
-rw-r--r--src/mongo/db/session/logical_session_cache.h150
-rw-r--r--src/mongo/db/session/logical_session_cache.idl75
-rw-r--r--src/mongo/db/session/logical_session_cache_impl.cpp460
-rw-r--r--src/mongo/db/session/logical_session_cache_impl.h122
-rw-r--r--src/mongo/db/session/logical_session_cache_noop.h81
-rw-r--r--src/mongo/db/session/logical_session_cache_stats.idl76
-rw-r--r--src/mongo/db/session/logical_session_cache_test.cpp448
-rw-r--r--src/mongo/db/session/logical_session_id.cpp70
-rw-r--r--src/mongo/db/session/logical_session_id.h198
-rw-r--r--src/mongo/db/session/logical_session_id.idl245
-rw-r--r--src/mongo/db/session/logical_session_id_helpers.cpp271
-rw-r--r--src/mongo/db/session/logical_session_id_helpers.h135
-rw-r--r--src/mongo/db/session/logical_session_id_test.cpp451
-rw-r--r--src/mongo/db/session/session.h79
-rw-r--r--src/mongo/db/session/session_catalog.cpp556
-rw-r--r--src/mongo/db/session/session_catalog.h521
-rw-r--r--src/mongo/db/session/session_catalog_mongod.cpp726
-rw-r--r--src/mongo/db/session/session_catalog_mongod.h159
-rw-r--r--src/mongo/db/session/session_catalog_mongod_test.cpp93
-rw-r--r--src/mongo/db/session/session_catalog_test.cpp1658
-rw-r--r--src/mongo/db/session/session_killer.cpp234
-rw-r--r--src/mongo/db/session/session_killer.h153
-rw-r--r--src/mongo/db/session/session_txn_record.idl89
-rw-r--r--src/mongo/db/session/sessions_collection.cpp302
-rw-r--r--src/mongo/db/session/sessions_collection.h133
-rw-r--r--src/mongo/db/session/sessions_collection_mock.cpp113
-rw-r--r--src/mongo/db/session/sessions_collection_mock.h131
-rw-r--r--src/mongo/db/session/sessions_collection_rs.cpp240
-rw-r--r--src/mongo/db/session/sessions_collection_rs.h115
-rw-r--r--src/mongo/db/session/sessions_collection_standalone.cpp128
-rw-r--r--src/mongo/db/session/sessions_collection_standalone.h71
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