diff options
author | Max Hirschhorn <max.hirschhorn@mongodb.com> | 2021-03-18 00:17:09 +0000 |
---|---|---|
committer | Evergreen Agent <no-reply@evergreen.mongodb.com> | 2021-03-18 00:52:19 +0000 |
commit | 5397acfdb865a7daa605c4f248822d7dbe74bffc (patch) | |
tree | d546c7fcf0941f3c119a7e4952353565fede9ac0 | |
parent | 2a19952b4522489d35e4931eb10c5e74a8bd2bc2 (diff) | |
download | mongo-5397acfdb865a7daa605c4f248822d7dbe74bffc.tar.gz |
SERVER-55102 Create CancelableOperationContext class.
-rw-r--r-- | src/mongo/db/SConscript | 2 | ||||
-rw-r--r-- | src/mongo/db/cancelable_operation_context.cpp | 65 | ||||
-rw-r--r-- | src/mongo/db/cancelable_operation_context.h | 95 | ||||
-rw-r--r-- | src/mongo/db/cancelable_operation_context_test.cpp | 209 |
4 files changed, 371 insertions, 0 deletions
diff --git a/src/mongo/db/SConscript b/src/mongo/db/SConscript index c9a65ce3bd5..00520a3b284 100644 --- a/src/mongo/db/SConscript +++ b/src/mongo/db/SConscript @@ -452,6 +452,7 @@ env.Library( target='service_context', source=[ 'baton.cpp', + 'cancelable_operation_context.cpp', 'client.cpp', 'client_strand.cpp', 'default_baton.cpp', @@ -2304,6 +2305,7 @@ if wiredtiger: envWithAsio.CppUnitTest( target='db_unittest_test', source=[ + 'cancelable_operation_context_test.cpp', 'catalog_raii_test.cpp', 'client_strand_test.cpp', 'client_context_test.cpp', diff --git a/src/mongo/db/cancelable_operation_context.cpp b/src/mongo/db/cancelable_operation_context.cpp new file mode 100644 index 00000000000..c929f33f9e6 --- /dev/null +++ b/src/mongo/db/cancelable_operation_context.cpp @@ -0,0 +1,65 @@ +/** + * 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/cancelable_operation_context.h" + +#include "mongo/db/client.h" +#include "mongo/db/operation_context.h" +#include "mongo/stdx/mutex.h" +#include "mongo/util/cancelation.h" + +namespace mongo { + +CancelableOperationContext::CancelableOperationContext(ServiceContext::UniqueOperationContext opCtx, + const CancelationToken& cancelToken, + ExecutorPtr executor) + : _sharedBlock{std::make_shared<SharedBlock>()}, + _opCtx{std::move(opCtx)}, + _markKilledFinished{cancelToken.onCancel() + .thenRunOn(std::move(executor)) + .then([sharedBlock = _sharedBlock, opCtx = _opCtx.get()] { + if (!sharedBlock->done.swap(true)) { + stdx::lock_guard<Client> lk(*opCtx->getClient()); + opCtx->markKilled(ErrorCodes::CallbackCanceled); + } + }) + .semi()} {} + +CancelableOperationContext::~CancelableOperationContext() { + if (_sharedBlock->done.swap(true)) { + // _sharedBlock->done was already true so our onCancel() continuation must have started to + // run. We must wait for it to finish running to avoid markKilled() being called while the + // OperationContext is being destructed. + _markKilledFinished.wait(); + } +} + +} // namespace mongo diff --git a/src/mongo/db/cancelable_operation_context.h b/src/mongo/db/cancelable_operation_context.h new file mode 100644 index 00000000000..ca9493a58c2 --- /dev/null +++ b/src/mongo/db/cancelable_operation_context.h @@ -0,0 +1,95 @@ +/** + * 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 <memory> + +#include "mongo/db/service_context.h" +#include "mongo/platform/atomic_word.h" +#include "mongo/util/future.h" +#include "mongo/util/out_of_line_executor.h" + +namespace mongo { + +class CancelationToken; +class OperationContext; + +/** + * Wrapper class around an OperationContext that calls markKilled(ErrorCodes::CallbackCanceled) when + * the supplied CancelationToken is canceled. + * + * This class is useful for having an OperationContext be interrupted when a CancelationToken is + * canceled. Note that OperationContext::getCancelationToken() is instead useful for having a + * CancelationToken be canceled when an OperationContext is interrupted. The combination of the two + * enables bridging between OperationContext interruption and CancelationToken cancellation + * arbitrarily. + * + * IMPORTANT: Executors are allowed to refuse work. markKilled(ErrorCodes::CallbackCanceled) won't + * be called when the supplied CancelationToken is canceled if the task executor has already been + * shut down, for example. Use a task executor bound to the process lifetime if you must guarantee + * that the OperationContext is interrupted when the CancelationToken is canceled. + */ +class CancelableOperationContext { +public: + CancelableOperationContext(ServiceContext::UniqueOperationContext opCtx, + const CancelationToken& cancelToken, + ExecutorPtr executor); + + CancelableOperationContext(const CancelableOperationContext&) = delete; + CancelableOperationContext& operator=(const CancelableOperationContext&) = delete; + + CancelableOperationContext(CancelableOperationContext&&) = delete; + CancelableOperationContext& operator=(CancelableOperationContext&&) = delete; + + ~CancelableOperationContext(); + + OperationContext* get() const noexcept { + return _opCtx.get(); + } + + OperationContext* operator->() const noexcept { + return get(); + } + + OperationContext& operator*() const noexcept { + return *get(); + } + +private: + struct SharedBlock { + AtomicWord<bool> done{false}; + }; + + const std::shared_ptr<SharedBlock> _sharedBlock; + const ServiceContext::UniqueOperationContext _opCtx; + const SemiFuture<void> _markKilledFinished; +}; + +} // namespace mongo diff --git a/src/mongo/db/cancelable_operation_context_test.cpp b/src/mongo/db/cancelable_operation_context_test.cpp new file mode 100644 index 00000000000..bcdf7f5e22d --- /dev/null +++ b/src/mongo/db/cancelable_operation_context_test.cpp @@ -0,0 +1,209 @@ +/** + * 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/cancelable_operation_context.h" +#include "mongo/db/client.h" +#include "mongo/db/operation_context.h" +#include "mongo/stdx/mutex.h" +#include "mongo/unittest/unittest.h" +#include "mongo/util/concurrency/thread_pool.h" + +namespace mongo { +namespace { + +class CancelableOperationContextTest : public unittest::Test { +public: + void setUp() override { + ThreadPool::Options options; + options.poolName = "CancelableOperationContextTest"; + options.minThreads = 1; + options.maxThreads = 1; + + _threadPool = std::make_shared<ThreadPool>(std::move(options)); + _threadPool->startup(); + } + + void tearDown() override { + _threadPool->shutdown(); + _threadPool->join(); + _threadPool.reset(); + } + + ExecutorPtr executor() { + return _threadPool; + } + + void waitForAllEarlierTasksToComplete() { + _threadPool->waitForIdle(); + } + + void shutDownExecutor() { + _threadPool->shutdown(); + } + +private: + std::shared_ptr<ThreadPool> _threadPool; +}; + +TEST_F(CancelableOperationContextTest, ActsAsNormalOperationContext) { + auto serviceCtx = ServiceContext::make(); + auto client = serviceCtx->makeClient("CancelableOperationContextTest"); + auto opCtx = CancelableOperationContext{ + client->makeOperationContext(), CancelationToken::uncancelable(), executor()}; + + ASSERT_EQ(opCtx->getClient(), client.get()); + ASSERT_EQ(opCtx.get()->getClient(), client.get()); + + // The CancelationSource underlying the OperationContext* is unassociated with the one supplied + // to the CancelableOperationContext constructor. + ASSERT_TRUE(opCtx->getCancelationToken().isCancelable()); +} + +TEST_F(CancelableOperationContextTest, KilledWhenCancelationSourceIsCanceled) { + auto serviceCtx = ServiceContext::make(); + auto client = serviceCtx->makeClient("CancelableOperationContextTest"); + + CancelationSource cancelSource; + auto opCtx = CancelableOperationContext{ + client->makeOperationContext(), cancelSource.token(), executor()}; + + ASSERT_OK(opCtx->checkForInterruptNoAssert()); + + cancelSource.cancel(); + waitForAllEarlierTasksToComplete(); + ASSERT_EQ(opCtx->checkForInterruptNoAssert(), ErrorCodes::CallbackCanceled); +} + +TEST_F(CancelableOperationContextTest, SafeWhenCancelationSourceIsCanceledUnderClientMutex) { + auto serviceCtx = ServiceContext::make(); + auto client = serviceCtx->makeClient("CancelableOperationContextTest"); + + CancelationSource cancelSource; + auto opCtx = CancelableOperationContext{ + client->makeOperationContext(), cancelSource.token(), executor()}; + + ASSERT_OK(opCtx->checkForInterruptNoAssert()); + + { + // Holding the Client mutex while canceling the CancelationSource won't lead to + // self-deadlock. + stdx::lock_guard<Client> lk(*opCtx->getClient()); + cancelSource.cancel(); + } + waitForAllEarlierTasksToComplete(); + ASSERT_EQ(opCtx->checkForInterruptNoAssert(), ErrorCodes::CallbackCanceled); +} + +TEST_F(CancelableOperationContextTest, SafeWhenDestructedBeforeCancelationSourceIsCanceled) { + auto serviceCtx = ServiceContext::make(); + auto client = serviceCtx->makeClient("CancelableOperationContextTest"); + + CancelationSource cancelSource; + boost::optional<CancelableOperationContext> opCtx; + opCtx.emplace(client->makeOperationContext(), cancelSource.token(), executor()); + + opCtx.reset(); + cancelSource.cancel(); +} + +TEST_F(CancelableOperationContextTest, NotKilledWhenCancelationSourceIsDestructed) { + auto serviceCtx = ServiceContext::make(); + auto client = serviceCtx->makeClient("CancelableOperationContextTest"); + + boost::optional<CancelationSource> cancelSource; + cancelSource.emplace(); + auto opCtx = CancelableOperationContext{ + client->makeOperationContext(), cancelSource->token(), executor()}; + + ASSERT_OK(opCtx->checkForInterruptNoAssert()); + + cancelSource.reset(); + ASSERT_OK(opCtx->checkForInterruptNoAssert()); +} + +TEST_F(CancelableOperationContextTest, + NotKilledWhenCancelationSourceIsCanceledAndTaskExecutorAlreadyShutDown) { + auto serviceCtx = ServiceContext::make(); + auto client = serviceCtx->makeClient("CancelableOperationContextTest"); + + CancelationSource cancelSource; + auto opCtx = CancelableOperationContext{ + client->makeOperationContext(), cancelSource.token(), executor()}; + + ASSERT_OK(opCtx->checkForInterruptNoAssert()); + + shutDownExecutor(); + cancelSource.cancel(); + ASSERT_OK(opCtx->checkForInterruptNoAssert()); +} + +TEST_F(CancelableOperationContextTest, SafeWhenOperationContextOwnCancelationTokenIsUsed) { + auto serviceCtx = ServiceContext::make(); + auto client = serviceCtx->makeClient("CancelableOperationContextTest"); + + auto opCtx = client->makeOperationContext(); + auto cancelToken = opCtx->getCancelationToken(); + auto cancelableOpCtx = CancelableOperationContext{std::move(opCtx), cancelToken, executor()}; + + ASSERT_OK(cancelableOpCtx->checkForInterruptNoAssert()); + + auto expectedErrorCode = ErrorCodes::Error(5510299); + { + // Acquiring the Client mutex is technically unnecessary here but we do it specifically to + // demonstrate that holding it won't lead to self-deadlock. + stdx::lock_guard<Client> lk(*cancelableOpCtx->getClient()); + cancelableOpCtx->markKilled(expectedErrorCode); + } + ASSERT_EQ(cancelableOpCtx->checkForInterruptNoAssert(), expectedErrorCode); +} + +TEST_F(CancelableOperationContextTest, SafeWhenOperationContextKilledManually) { + auto serviceCtx = ServiceContext::make(); + auto client = serviceCtx->makeClient("CancelableOperationContextTest"); + + CancelationSource cancelSource; + auto opCtx = CancelableOperationContext{ + client->makeOperationContext(), cancelSource.token(), executor()}; + + ASSERT_OK(opCtx->checkForInterruptNoAssert()); + + auto expectedErrorCode = ErrorCodes::Error(5510298); + { + // Acquiring the Client mutex is technically unnecessary here but we do it specifically to + // demonstrate that holding it won't lead to self-deadlock. + stdx::lock_guard<Client> lk(*opCtx->getClient()); + opCtx->markKilled(expectedErrorCode); + } + ASSERT_EQ(opCtx->checkForInterruptNoAssert(), expectedErrorCode); +} + +} // namespace +} // namespace mongo |