From 285d4e6e9b8ee2d185c7315d67999d1b109ed3c5 Mon Sep 17 00:00:00 2001 From: Alex Kremer Date: Fri, 20 Dec 2024 13:24:01 +0000 Subject: [PATCH] feat: Repeating operations for util::async (#1776) Async framework needed a way to do repeating operations (think simplest cases like AmendmentBlockHandler). This PR implements the **absolute minimum**, barebones repeating operations that - Can't return any values (void) - Do not take any arguments in the user-provided function - Can't be scheduled (i.e. a delay before starting repeating) - Can't be stopped from inside the user block of code (i.e. does not have stop token or anything like that) - Can be stopped through the RepeatedOperation's `abort()` function (but not from the user-provided repeating function) --- src/rpc/RPCHelpers.cpp | 2 +- src/rpc/RPCHelpers.hpp | 2 +- src/util/Repeat.cpp | 18 ++---- src/util/Repeat.hpp | 45 +++++++++------ src/util/async/AnyExecutionContext.hpp | 36 +++++++++++- src/util/async/Concepts.hpp | 25 +++++++- src/util/async/Operation.hpp | 54 ++++++++++++++++++ .../async/context/BasicExecutionContext.hpp | 29 +++++++--- src/util/async/impl/ErasedOperation.hpp | 34 ++++++++--- src/web/dosguard/IntervalSweepHandler.cpp | 3 +- tests/common/util/MockExecutionContext.hpp | 10 ++++ tests/common/util/MockOperation.hpp | 7 +++ tests/unit/data/BackendInterfaceTests.cpp | 2 +- tests/unit/util/RepeatTests.cpp | 15 +---- .../util/async/AnyExecutionContextTests.cpp | 57 ++++++++++++------- tests/unit/util/async/AnyOperationTests.cpp | 15 +++++ .../util/async/AsyncExecutionContextTests.cpp | 28 +++++++++ 17 files changed, 292 insertions(+), 90 deletions(-) diff --git a/src/rpc/RPCHelpers.cpp b/src/rpc/RPCHelpers.cpp index 08bba911..7e04787f 100644 --- a/src/rpc/RPCHelpers.cpp +++ b/src/rpc/RPCHelpers.cpp @@ -45,7 +45,6 @@ #include #include #include -#include #include #include #include @@ -75,6 +74,7 @@ #include #include #include +#include #include #include #include diff --git a/src/rpc/RPCHelpers.hpp b/src/rpc/RPCHelpers.hpp index 6a4c00f9..289a3d6e 100644 --- a/src/rpc/RPCHelpers.hpp +++ b/src/rpc/RPCHelpers.hpp @@ -40,7 +40,6 @@ #include #include #include -#include #include #include #include @@ -61,6 +60,7 @@ #include #include #include +#include #include #include diff --git a/src/util/Repeat.cpp b/src/util/Repeat.cpp index 118d5f1a..9b55b9fd 100644 --- a/src/util/Repeat.cpp +++ b/src/util/Repeat.cpp @@ -19,24 +19,16 @@ #include "util/Repeat.hpp" -#include - namespace util { -Repeat::Repeat(boost::asio::io_context& ioc) : timer_(ioc) -{ -} - -Repeat::~Repeat() -{ - *stopped_ = true; -} - void Repeat::stop() { - *stopped_ = true; - timer_.cancel(); + if (control_->stopping) + return; + control_->stopping = true; + control_->timer.cancel(); + control_->semaphore.acquire(); } } // namespace util diff --git a/src/util/Repeat.hpp b/src/util/Repeat.hpp index 56c54a50..79730c41 100644 --- a/src/util/Repeat.hpp +++ b/src/util/Repeat.hpp @@ -29,6 +29,7 @@ #include #include #include +#include namespace util { @@ -37,21 +38,35 @@ namespace util { * @note io_context must be stopped before the Repeat object is destroyed. Otherwise it is undefined behavior */ class Repeat { - boost::asio::steady_timer timer_; - std::shared_ptr stopped_ = std::make_shared(true); + struct Control { + boost::asio::steady_timer timer; + std::atomic_bool stopping{true}; + std::binary_semaphore semaphore{0}; + + Control(auto& ctx) : timer(ctx) + { + } + }; + + std::unique_ptr control_; public: /** * @brief Construct a new Repeat object + * @note The `ctx` parameter is `auto` so that this util supports `strand` and `thread_pool` as well as `io_context` * - * @param ioc The io_context to use + * @param ctx The io_context-like object to use */ - Repeat(boost::asio::io_context& ioc); + Repeat(auto& ctx) : control_(std::make_unique(ctx)) + { + } - /** - * @brief Destroy the Repeat object - */ - ~Repeat(); + Repeat(Repeat const&) = delete; + Repeat& + operator=(Repeat const&) = delete; + Repeat(Repeat&&) = default; + Repeat& + operator=(Repeat&&) = default; /** * @brief Stop repeating @@ -72,9 +87,8 @@ public: void start(std::chrono::steady_clock::duration interval, Action&& action) { - ASSERT(*stopped_, "Repeat should be stopped before the next use"); - // Create a new variable for each start() to make each start()-stop() session independent - stopped_ = std::make_shared(false); + ASSERT(control_->stopping, "Should be stopped before starting"); + control_->stopping = false; startImpl(interval, std::forward(action)); } @@ -83,11 +97,10 @@ private: void startImpl(std::chrono::steady_clock::duration interval, Action&& action) { - timer_.expires_after(interval); - timer_.async_wait([this, interval, stopping = stopped_, action = std::forward(action)]( - auto const& errorCode - ) mutable { - if (errorCode or *stopping) { + control_->timer.expires_after(interval); + control_->timer.async_wait([this, interval, action = std::forward(action)](auto const& ec) mutable { + if (ec or control_->stopping) { + control_->semaphore.release(); return; } action(); diff --git a/src/util/async/AnyExecutionContext.hpp b/src/util/async/AnyExecutionContext.hpp index faae1dec..7b9ffe8c 100644 --- a/src/util/async/AnyExecutionContext.hpp +++ b/src/util/async/AnyExecutionContext.hpp @@ -165,7 +165,7 @@ public: using RetType = std::decay_t()))>; static_assert(not std::is_same_v); - auto millis = std::chrono::duration_cast(delay); + auto const millis = std::chrono::duration_cast(delay); return AnyOperation(pimpl_->scheduleAfter( millis, [fn = std::forward(fn)](auto stopToken) -> std::any { @@ -195,7 +195,7 @@ public: using RetType = std::decay_t(), true))>; static_assert(not std::is_same_v); - auto millis = std::chrono::duration_cast(delay); + auto const millis = std::chrono::duration_cast(delay); return AnyOperation(pimpl_->scheduleAfter( millis, [fn = std::forward(fn)](auto stopToken, auto cancelled) -> std::any { @@ -209,6 +209,31 @@ public: )); } + /** + * @brief Schedule a repeating operation on the execution context + * + * @param interval The interval at which the operation should be repeated + * @param fn The block of code to execute; no args allowed and return type must be void + * @return A repeating stoppable operation that can be used to wait for its cancellation + */ + [[nodiscard]] auto + executeRepeatedly(SomeStdDuration auto interval, SomeHandlerWithoutStopToken auto&& fn) + { + using RetType = std::decay_t; + static_assert(not std::is_same_v); + + auto const millis = std::chrono::duration_cast(interval); + return AnyOperation( // + pimpl_->executeRepeatedly( + millis, + [fn = std::forward(fn)] -> std::any { + fn(); + return {}; + } + ) + ); + } + /** * @brief Make a strand for this execution context * @@ -255,6 +280,7 @@ private: scheduleAfter(std::chrono::milliseconds, std::function) = 0; virtual impl::ErasedOperation scheduleAfter(std::chrono::milliseconds, std::function) = 0; + virtual impl::ErasedOperation executeRepeatedly(std::chrono::milliseconds, std::function) = 0; virtual AnyStrand makeStrand() = 0; virtual void @@ -296,6 +322,12 @@ private: return ctx.scheduleAfter(delay, std::move(fn)); } + impl::ErasedOperation + executeRepeatedly(std::chrono::milliseconds interval, std::function fn) override + { + return ctx.executeRepeatedly(interval, std::move(fn)); + } + AnyStrand makeStrand() override { diff --git a/src/util/async/Concepts.hpp b/src/util/async/Concepts.hpp index e0065dbc..b6b86038 100644 --- a/src/util/async/Concepts.hpp +++ b/src/util/async/Concepts.hpp @@ -45,12 +45,33 @@ concept SomeCancellable = requires(T v) { { v.cancel() } -> std::same_as; }; +/** + * @brief Specifies the interface for an operation that can be awaited + */ +template +concept SomeAwaitable = requires(T v) { + { v.wait() } -> std::same_as; +}; + +/** + * @brief Specifies the interface for an operation that can be aborted + */ +template +concept SomeAbortable = requires(T v) { + { v.abort() } -> std::same_as; +}; + /** * @brief Specifies the interface for an operation */ template -concept SomeOperation = requires(T v) { - { v.wait() } -> std::same_as; +concept SomeOperation = SomeAwaitable or SomeAbortable; + +/** + * @brief Specifies the interface for an operation + */ +template +concept SomeOperationWithData = SomeOperation and requires(T v) { { v.get() }; }; diff --git a/src/util/async/Operation.hpp b/src/util/async/Operation.hpp index ae8b8a65..ffda0efd 100644 --- a/src/util/async/Operation.hpp +++ b/src/util/async/Operation.hpp @@ -19,16 +19,24 @@ #pragma once +#include "util/Repeat.hpp" #include "util/async/Concepts.hpp" +#include "util/async/Error.hpp" #include "util/async/Outcome.hpp" #include "util/async/context/impl/Cancellation.hpp" #include "util/async/context/impl/Timer.hpp" +#include + +#include +#include #include +#include #include #include #include #include +#include namespace util::async { namespace impl { @@ -181,4 +189,50 @@ using Operation = impl::BasicOperation>; template using ScheduledOperation = impl::BasicScheduledOperation; +/** + * @brief The `future` side of async operations that automatically repeat until aborted + * + * @note The current implementation requires the user provided function to return void and to take no arguments. There + * is also no mechanism to request the repeating task to stop from inside of the user provided block of code. + * + * @tparam CtxType The type of the execution context + */ +template +class RepeatingOperation { + util::Repeat repeat_; + +public: + /** + * @brief Construct a new Repeating Operation object + * @note The first invocation of the user-provided function happens with no delay + * + * @param executor The executor to operate on + * @param interval Time to wait before repeating the user-provided block of code + * @param fn The function to execute repeatedly + */ + RepeatingOperation(auto& executor, std::chrono::steady_clock::duration interval, std::invocable auto&& fn) + : repeat_(executor) + { + repeat_.start(interval, std::forward(fn)); + } + + RepeatingOperation(RepeatingOperation const&) = delete; + RepeatingOperation& + operator=(RepeatingOperation const&) = delete; + RepeatingOperation(RepeatingOperation&&) = default; + RepeatingOperation& + operator=(RepeatingOperation&&) = default; + + /** + * @brief Aborts the operation and the repeating timer + * @note This call blocks until the underlying timer is cancelled + * @warning Calling this from inside of the repeating operation yields a deadlock + */ + void + abort() noexcept + { + repeat_.stop(); + } +}; + } // namespace util::async diff --git a/src/util/async/context/BasicExecutionContext.hpp b/src/util/async/context/BasicExecutionContext.hpp index 53370db4..9a650a58 100644 --- a/src/util/async/context/BasicExecutionContext.hpp +++ b/src/util/async/context/BasicExecutionContext.hpp @@ -162,15 +162,13 @@ public: using Timer = typename ContextHolderType::Timer; - /** - * @brief Create a new execution context with the given number of threads. - * - * Note: scheduled operations are always stoppable - * @tparam T The type of the value returned by operations - */ + // note: scheduled operations are always stoppable template using ScheduledOperation = ScheduledOperation>; + // note: repeating operations are always stoppable and must return void + using RepeatedOperation = RepeatingOperation; + /** * @brief Create a new execution context with the given number of threads. * @@ -181,7 +179,7 @@ public: } /** - * @brief Stops and joins the underlying thread pool. + * @brief Stops the underlying thread pool. */ ~BasicExecutionContext() { @@ -272,6 +270,23 @@ public: } } + /** + * @brief Schedule a repeating operation on the execution context + * + * @param interval The interval at which the operation should be repeated + * @param fn The block of code to execute; no args allowed and return type must be void + * @return A repeating stoppable operation that can be used to wait for its cancellation + */ + [[nodiscard]] auto + executeRepeatedly(SomeStdDuration auto interval, SomeHandlerWithoutStopToken auto&& fn) noexcept(isNoexcept) + { + if constexpr (not std::is_same_v) { + return TimerContextProvider::getContext(*this).executeRepeatedly(interval, std::forward(fn)); + } else { + return RepeatedOperation(impl::extractAssociatedExecutor(*this), interval, std::forward(fn)); + } + } + /** * @brief Schedule an operation on the execution context * diff --git a/src/util/async/impl/ErasedOperation.hpp b/src/util/async/impl/ErasedOperation.hpp index f4fc9f5f..480295dd 100644 --- a/src/util/async/impl/ErasedOperation.hpp +++ b/src/util/async/impl/ErasedOperation.hpp @@ -27,6 +27,7 @@ #include #include #include +#include namespace util::async::impl { @@ -95,26 +96,41 @@ private: void wait() noexcept override { - return operation.wait(); + if constexpr (not SomeAwaitable) { + ASSERT(false, "Called wait() on an operation that does not support it"); + std::unreachable(); + } else { + operation.wait(); + } } std::expected get() override { - // Note: return type of the operation was already wrapped to std::any by AnyExecutionContext - return operation.get(); + if constexpr (not SomeOperationWithData) { + ASSERT(false, "Called get() on an operation that does not support it"); + std::unreachable(); + } else { + // Note: return type of the operation was already wrapped to std::any by AnyExecutionContext + return operation.get(); + } } void abort() override { - if constexpr (not SomeCancellableOperation and not SomeStoppableOperation) { - ASSERT(false, "Called abort() on an operation that can't be cancelled nor stopped"); + if constexpr (not SomeCancellableOperation and not SomeStoppableOperation and + not SomeAbortable) { + ASSERT(false, "Called abort() on an operation that can't be aborted, cancelled nor stopped"); } else { - if constexpr (SomeCancellableOperation) - operation.cancel(); - if constexpr (SomeStoppableOperation) - operation.requestStop(); + if constexpr (SomeAbortable) { + operation.abort(); + } else { + if constexpr (SomeCancellableOperation) + operation.cancel(); + if constexpr (SomeStoppableOperation) + operation.requestStop(); + } } } }; diff --git a/src/web/dosguard/IntervalSweepHandler.cpp b/src/web/dosguard/IntervalSweepHandler.cpp index 348fc4dd..2049c077 100644 --- a/src/web/dosguard/IntervalSweepHandler.cpp +++ b/src/web/dosguard/IntervalSweepHandler.cpp @@ -27,7 +27,6 @@ #include #include -#include namespace web::dosguard { @@ -36,7 +35,7 @@ IntervalSweepHandler::IntervalSweepHandler( boost::asio::io_context& ctx, BaseDOSGuard& dosGuard ) - : repeat_{std::ref(ctx)} + : repeat_{ctx} { auto const sweepInterval{std::max( std::chrono::milliseconds{1u}, diff --git a/tests/common/util/MockExecutionContext.hpp b/tests/common/util/MockExecutionContext.hpp index 576f519a..41e5bc14 100644 --- a/tests/common/util/MockExecutionContext.hpp +++ b/tests/common/util/MockExecutionContext.hpp @@ -50,6 +50,9 @@ struct MockExecutionContext { template using ScheduledOperation = MockScheduledOperation; + template + using RepeatingOperation = MockRepeatingOperation; + MOCK_METHOD(Operation const&, execute, (std::function), ()); MOCK_METHOD( Operation const&, @@ -75,6 +78,13 @@ struct MockExecutionContext { (std::chrono::milliseconds, std::function), () ); + MOCK_METHOD( + RepeatingOperation const&, + executeRepeatedly, + (std::chrono::milliseconds, std::function), + () + ); + MOCK_METHOD(MockStrand const&, makeStrand, (), ()); MOCK_METHOD(void, stop, (), (const)); MOCK_METHOD(void, join, (), ()); diff --git a/tests/common/util/MockOperation.hpp b/tests/common/util/MockOperation.hpp index 0057e83f..34f3fdb7 100644 --- a/tests/common/util/MockOperation.hpp +++ b/tests/common/util/MockOperation.hpp @@ -42,3 +42,10 @@ struct MockScheduledOperation { MOCK_METHOD(ValueType, get, (), (const)); MOCK_METHOD(void, getToken, (), (const)); }; + +template +struct MockRepeatingOperation { + MOCK_METHOD(void, requestStop, (), (const)); + MOCK_METHOD(void, wait, (), (const)); + MOCK_METHOD(ValueType, get, (), (const)); +}; diff --git a/tests/unit/data/BackendInterfaceTests.cpp b/tests/unit/data/BackendInterfaceTests.cpp index dfbdfd4c..e6e12620 100644 --- a/tests/unit/data/BackendInterfaceTests.cpp +++ b/tests/unit/data/BackendInterfaceTests.cpp @@ -27,9 +27,9 @@ #include #include #include -#include #include #include +#include #include #include diff --git a/tests/unit/util/RepeatTests.cpp b/tests/unit/util/RepeatTests.cpp index 460ce185..8e7b5af7 100644 --- a/tests/unit/util/RepeatTests.cpp +++ b/tests/unit/util/RepeatTests.cpp @@ -54,7 +54,7 @@ struct RepeatTest : SyncAsioContextTest { TEST_F(RepeatTest, CallsHandler) { repeat.start(std::chrono::milliseconds{1}, handlerMock.AsStdFunction()); - EXPECT_CALL(handlerMock, Call).Times(AtLeast(10)); + EXPECT_CALL(handlerMock, Call).Times(testing::AtMost(22)); runContextFor(std::chrono::milliseconds{20}); } @@ -79,16 +79,3 @@ TEST_F(RepeatTest, RunsAfterStop) } }); } - -struct RepeatDeathTest : RepeatTest {}; - -TEST_F(RepeatDeathTest, DiesWhenStartCalledTwice) -{ - EXPECT_DEATH( - { - repeat.start(std::chrono::seconds{1}, []() {}); - repeat.start(std::chrono::seconds{1}, []() {}); - }, - "Assertion .* failed.*" - ); -} diff --git a/tests/unit/util/async/AnyExecutionContextTests.cpp b/tests/unit/util/async/AnyExecutionContextTests.cpp index b10fa2d3..6e862f25 100644 --- a/tests/unit/util/async/AnyExecutionContextTests.cpp +++ b/tests/unit/util/async/AnyExecutionContextTests.cpp @@ -115,6 +115,9 @@ struct AnyExecutionContextTests : Test { template using ScheduledOperationType = NiceMock>; + template + using RepeatingOperationType = NiceMock>; + NiceMock mockExecutionContext; AnyExecutionContext ctx{static_cast(mockExecutionContext)}; }; @@ -122,7 +125,7 @@ struct AnyExecutionContextTests : Test { TEST_F(AnyExecutionContextTests, Move) { auto mockOp = OperationType{}; - EXPECT_CALL(mockExecutionContext, execute(An>())).WillOnce(ReturnRef(mockOp)); + EXPECT_CALL(mockExecutionContext, execute(A>())).WillOnce(ReturnRef(mockOp)); EXPECT_CALL(mockOp, get()); auto mineNow = std::move(ctx); @@ -132,7 +135,7 @@ TEST_F(AnyExecutionContextTests, Move) TEST_F(AnyExecutionContextTests, CopyIsRefCounted) { auto mockOp = OperationType{}; - EXPECT_CALL(mockExecutionContext, execute(An>())).WillOnce(ReturnRef(mockOp)); + EXPECT_CALL(mockExecutionContext, execute(A>())).WillOnce(ReturnRef(mockOp)); EXPECT_CALL(mockOp, get()); auto yoink = ctx; @@ -142,7 +145,7 @@ TEST_F(AnyExecutionContextTests, CopyIsRefCounted) TEST_F(AnyExecutionContextTests, ExecuteWithoutTokenAndVoid) { auto mockOp = OperationType{}; - EXPECT_CALL(mockExecutionContext, execute(An>())).WillOnce(ReturnRef(mockOp)); + EXPECT_CALL(mockExecutionContext, execute(A>())).WillOnce(ReturnRef(mockOp)); EXPECT_CALL(mockOp, get()); auto op = ctx.execute([] { throw 0; }); @@ -154,7 +157,7 @@ TEST_F(AnyExecutionContextTests, ExecuteWithoutTokenAndVoid) TEST_F(AnyExecutionContextTests, ExecuteWithoutTokenAndVoidThrowsException) { auto mockOp = OperationType{}; - EXPECT_CALL(mockExecutionContext, execute(An>())) + EXPECT_CALL(mockExecutionContext, execute(A>())) .WillOnce([](auto&&) -> OperationType const& { throw 0; }); EXPECT_ANY_THROW([[maybe_unused]] auto unused = ctx.execute([] { throw 0; })); @@ -163,7 +166,7 @@ TEST_F(AnyExecutionContextTests, ExecuteWithoutTokenAndVoidThrowsException) TEST_F(AnyExecutionContextTests, ExecuteWithStopTokenAndVoid) { auto mockOp = StoppableOperationType{}; - EXPECT_CALL(mockExecutionContext, execute(An>(), _)) + EXPECT_CALL(mockExecutionContext, execute(A>(), _)) .WillOnce(ReturnRef(mockOp)); EXPECT_CALL(mockOp, get()); @@ -175,7 +178,7 @@ TEST_F(AnyExecutionContextTests, ExecuteWithStopTokenAndVoid) TEST_F(AnyExecutionContextTests, ExecuteWithStopTokenAndVoidThrowsException) { - EXPECT_CALL(mockExecutionContext, execute(An>(), _)) + EXPECT_CALL(mockExecutionContext, execute(A>(), _)) .WillOnce([](auto&&, auto) -> StoppableOperationType const& { throw 0; }); EXPECT_ANY_THROW([[maybe_unused]] auto unused = ctx.execute([](auto) { throw 0; })); @@ -185,7 +188,7 @@ TEST_F(AnyExecutionContextTests, ExecuteWithStopTokenAndReturnValue) { auto mockOp = StoppableOperationType{}; EXPECT_CALL(mockOp, get()).WillOnce(Return(std::make_any(42))); - EXPECT_CALL(mockExecutionContext, execute(An>(), _)) + EXPECT_CALL(mockExecutionContext, execute(A>(), _)) .WillOnce(ReturnRef(mockOp)); auto op = ctx.execute([](auto) -> int { throw 0; }); @@ -196,7 +199,7 @@ TEST_F(AnyExecutionContextTests, ExecuteWithStopTokenAndReturnValue) TEST_F(AnyExecutionContextTests, ExecuteWithStopTokenAndReturnValueThrowsException) { - EXPECT_CALL(mockExecutionContext, execute(An>(), _)) + EXPECT_CALL(mockExecutionContext, execute(A>(), _)) .WillOnce([](auto&&, auto) -> StoppableOperationType const& { throw 0; }); EXPECT_ANY_THROW([[maybe_unused]] auto unused = ctx.execute([](auto) -> int { throw 0; })); @@ -207,8 +210,7 @@ TEST_F(AnyExecutionContextTests, TimerCancellation) auto mockScheduledOp = ScheduledOperationType{}; EXPECT_CALL(mockScheduledOp, cancel()); EXPECT_CALL( - mockExecutionContext, - scheduleAfter(An(), An>()) + mockExecutionContext, scheduleAfter(std::chrono::milliseconds{12}, A>()) ) .WillOnce(ReturnRef(mockScheduledOp)); @@ -223,8 +225,7 @@ TEST_F(AnyExecutionContextTests, TimerExecuted) auto mockScheduledOp = ScheduledOperationType{}; EXPECT_CALL(mockScheduledOp, get()).WillOnce(Return(std::make_any(42))); EXPECT_CALL( - mockExecutionContext, - scheduleAfter(An(), An>()) + mockExecutionContext, scheduleAfter(std::chrono::milliseconds{12}, A>()) ) .WillOnce([&mockScheduledOp](auto, auto&&) -> ScheduledOperationType const& { return mockScheduledOp; @@ -242,7 +243,7 @@ TEST_F(AnyExecutionContextTests, TimerWithBoolHandlerCancellation) EXPECT_CALL(mockScheduledOp, cancel()); EXPECT_CALL( mockExecutionContext, - scheduleAfter(An(), An>()) + scheduleAfter(std::chrono::milliseconds{12}, A>()) ) .WillOnce(ReturnRef(mockScheduledOp)); @@ -258,7 +259,7 @@ TEST_F(AnyExecutionContextTests, TimerWithBoolHandlerExecuted) EXPECT_CALL(mockScheduledOp, get()).WillOnce(Return(std::make_any(42))); EXPECT_CALL( mockExecutionContext, - scheduleAfter(An(), An>()) + scheduleAfter(std::chrono::milliseconds{12}, A>()) ) .WillOnce([&mockScheduledOp](auto, auto&&) -> ScheduledOperationType const& { return mockScheduledOp; @@ -270,13 +271,25 @@ TEST_F(AnyExecutionContextTests, TimerWithBoolHandlerExecuted) EXPECT_EQ(timer.get().value(), 42); } +TEST_F(AnyExecutionContextTests, RepeatingOperation) +{ + auto mockRepeatingOp = RepeatingOperationType{}; + EXPECT_CALL(mockRepeatingOp, wait()); + EXPECT_CALL(mockExecutionContext, executeRepeatedly(std::chrono::milliseconds{1}, A>())) + .WillOnce([&mockRepeatingOp] -> RepeatingOperationType const& { return mockRepeatingOp; }); + + auto res = ctx.executeRepeatedly(std::chrono::milliseconds{1}, [] -> void { throw 0; }); + static_assert(std::is_same_v>); + res.wait(); +} + TEST_F(AnyExecutionContextTests, StrandExecuteWithVoid) { auto mockOp = OperationType{}; auto mockStrand = StrandType{}; EXPECT_CALL(mockOp, get()); EXPECT_CALL(mockExecutionContext, makeStrand()).WillOnce(ReturnRef(mockStrand)); - EXPECT_CALL(mockStrand, execute(An>())).WillOnce(ReturnRef(mockOp)); + EXPECT_CALL(mockStrand, execute(A>())).WillOnce(ReturnRef(mockOp)); auto strand = ctx.makeStrand(); static_assert(std::is_same_v); @@ -291,7 +304,7 @@ TEST_F(AnyExecutionContextTests, StrandExecuteWithVoidThrowsException) { auto mockStrand = StrandType{}; EXPECT_CALL(mockExecutionContext, makeStrand()).WillOnce(ReturnRef(mockStrand)); - EXPECT_CALL(mockStrand, execute(An>())) + EXPECT_CALL(mockStrand, execute(A>())) .WillOnce([](auto&&) -> OperationType const& { throw 0; }); auto strand = ctx.makeStrand(); @@ -306,7 +319,7 @@ TEST_F(AnyExecutionContextTests, StrandExecuteWithReturnValue) auto mockStrand = StrandType{}; EXPECT_CALL(mockOp, get()).WillOnce(Return(std::make_any(42))); EXPECT_CALL(mockExecutionContext, makeStrand()).WillOnce(ReturnRef(mockStrand)); - EXPECT_CALL(mockStrand, execute(An>())).WillOnce(ReturnRef(mockOp)); + EXPECT_CALL(mockStrand, execute(A>())).WillOnce(ReturnRef(mockOp)); auto strand = ctx.makeStrand(); static_assert(std::is_same_v); @@ -321,7 +334,7 @@ TEST_F(AnyExecutionContextTests, StrandExecuteWithReturnValueThrowsException) { auto mockStrand = StrandType{}; EXPECT_CALL(mockExecutionContext, makeStrand()).WillOnce(ReturnRef(mockStrand)); - EXPECT_CALL(mockStrand, execute(An>())) + EXPECT_CALL(mockStrand, execute(A>())) .WillOnce([](auto&&) -> OperationType const& { throw 0; }); auto strand = ctx.makeStrand(); @@ -336,7 +349,7 @@ TEST_F(AnyExecutionContextTests, StrandExecuteWithStopTokenAndVoid) auto mockStrand = StrandType{}; EXPECT_CALL(mockOp, get()); EXPECT_CALL(mockExecutionContext, makeStrand()).WillOnce(ReturnRef(mockStrand)); - EXPECT_CALL(mockStrand, execute(An>(), _)).WillOnce(ReturnRef(mockOp)); + EXPECT_CALL(mockStrand, execute(A>(), _)).WillOnce(ReturnRef(mockOp)); auto strand = ctx.makeStrand(); static_assert(std::is_same_v); @@ -351,7 +364,7 @@ TEST_F(AnyExecutionContextTests, StrandExecuteWithStopTokenAndVoidThrowsExceptio { auto mockStrand = StrandType{}; EXPECT_CALL(mockExecutionContext, makeStrand()).WillOnce(ReturnRef(mockStrand)); - EXPECT_CALL(mockStrand, execute(An>(), _)) + EXPECT_CALL(mockStrand, execute(A>(), _)) .WillOnce([](auto&&, auto) -> StoppableOperationType const& { throw 0; }); auto strand = ctx.makeStrand(); @@ -366,7 +379,7 @@ TEST_F(AnyExecutionContextTests, StrandExecuteWithStopTokenAndReturnValue) auto mockStrand = StrandType{}; EXPECT_CALL(mockOp, get()).WillOnce(Return(std::make_any(42))); EXPECT_CALL(mockExecutionContext, makeStrand()).WillOnce(ReturnRef(mockStrand)); - EXPECT_CALL(mockStrand, execute(An>(), _)).WillOnce(ReturnRef(mockOp)); + EXPECT_CALL(mockStrand, execute(A>(), _)).WillOnce(ReturnRef(mockOp)); auto strand = ctx.makeStrand(); static_assert(std::is_same_v); @@ -381,7 +394,7 @@ TEST_F(AnyExecutionContextTests, StrandExecuteWithStopTokenAndReturnValueThrowsE { auto mockStrand = StrandType{}; EXPECT_CALL(mockExecutionContext, makeStrand()).WillOnce(ReturnRef(mockStrand)); - EXPECT_CALL(mockStrand, execute(An>(), _)) + EXPECT_CALL(mockStrand, execute(A>(), _)) .WillOnce([](auto&&, auto) -> StoppableOperationType const& { throw 0; }); auto strand = ctx.makeStrand(); diff --git a/tests/unit/util/async/AnyOperationTests.cpp b/tests/unit/util/async/AnyOperationTests.cpp index 6ab386d2..bb35547b 100644 --- a/tests/unit/util/async/AnyOperationTests.cpp +++ b/tests/unit/util/async/AnyOperationTests.cpp @@ -36,15 +36,18 @@ struct AnyOperationTests : Test { using OperationType = MockOperation>; using StoppableOperationType = MockStoppableOperation>; using ScheduledOperationType = MockScheduledOperation>; + using RepeatingOperationType = MockRepeatingOperation>; NaggyMock mockOp; NaggyMock mockStoppableOp; NaggyMock mockScheduledOp; + NaggyMock mockRepeatingOp; AnyOperation voidOp{impl::ErasedOperation(static_cast(mockOp))}; AnyOperation voidStoppableOp{impl::ErasedOperation(static_cast(mockStoppableOp))}; AnyOperation intOp{impl::ErasedOperation(static_cast(mockOp))}; AnyOperation scheduledVoidOp{impl::ErasedOperation(static_cast(mockScheduledOp))}; + AnyOperation repeatingOp{impl::ErasedOperation(static_cast(mockRepeatingOp))}; }; using AnyOperationDeathTest = AnyOperationTests; @@ -113,6 +116,18 @@ TEST_F(AnyOperationTests, GetIncorrectDataReturnsError) EXPECT_TRUE(std::string{res.error()}.ends_with("Bad any cast")); } +TEST_F(AnyOperationTests, RepeatingOpWaitPropagated) +{ + EXPECT_CALL(mockRepeatingOp, wait()); + repeatingOp.wait(); +} + +TEST_F(AnyOperationTests, RepeatingOpRequestStopCallPropagated) +{ + EXPECT_CALL(mockRepeatingOp, requestStop()); + repeatingOp.abort(); +} + TEST_F(AnyOperationDeathTest, CallAbortOnNonStoppableOrCancellableOperation) { EXPECT_DEATH(voidOp.abort(), ".*"); diff --git a/tests/unit/util/async/AsyncExecutionContextTests.cpp b/tests/unit/util/async/AsyncExecutionContextTests.cpp index 2c259abb..d68e9200 100644 --- a/tests/unit/util/async/AsyncExecutionContextTests.cpp +++ b/tests/unit/util/async/AsyncExecutionContextTests.cpp @@ -17,15 +17,20 @@ */ //============================================================================== +#include "util/Profiler.hpp" +#include "util/async/Operation.hpp" #include "util/async/context/BasicExecutionContext.hpp" #include "util/async/context/SyncExecutionContext.hpp" +#include #include #include +#include #include #include #include +#include using namespace util::async; using ::testing::Types; @@ -36,6 +41,12 @@ template struct ExecutionContextTests : public ::testing::Test { using ExecutionContextType = T; ExecutionContextType ctx{2}; + + ~ExecutionContextTests() override + { + ctx.stop(); + ctx.join(); + } }; TYPED_TEST_CASE(ExecutionContextTests, ExecutionContextTypes); @@ -167,6 +178,23 @@ TYPED_TEST(ExecutionContextTests, timerUnknownException) EXPECT_TRUE(std::string{err}.ends_with("unknown")); } +TYPED_TEST(ExecutionContextTests, repeatingOperation) +{ + auto const repeatDelay = std::chrono::milliseconds{1}; + auto const timeout = std::chrono::milliseconds{15}; + auto callCount = 0uz; + + auto res = this->ctx.executeRepeatedly(repeatDelay, [&] { ++callCount; }); + auto timeSpent = util::timed([timeout] { std::this_thread::sleep_for(timeout); }); // calculate actual time spent + + res.abort(); // outside of the above stopwatch because it blocks and can take arbitrary time + auto const expectedPureCalls = timeout.count() / repeatDelay.count(); + auto const expectedActualCount = timeSpent / repeatDelay.count(); + + EXPECT_GE(callCount, expectedPureCalls / 2u); // expect at least half of the scheduled calls + EXPECT_LE(callCount, expectedActualCount); // never should be called more times than possible before timeout +} + TYPED_TEST(ExecutionContextTests, strandMove) { auto strand = this->ctx.makeStrand();