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)
This commit is contained in:
Alex Kremer
2024-12-20 13:24:01 +00:00
committed by GitHub
parent f2a89b095d
commit 285d4e6e9b
17 changed files with 292 additions and 90 deletions

View File

@@ -27,9 +27,9 @@
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <xrpl/basics/Blob.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/XRPAmount.h>
#include <optional>
#include <vector>

View File

@@ -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.*"
);
}

View File

@@ -115,6 +115,9 @@ struct AnyExecutionContextTests : Test {
template <typename T>
using ScheduledOperationType = NiceMock<MockScheduledOperation<T>>;
template <typename T>
using RepeatingOperationType = NiceMock<MockRepeatingOperation<T>>;
NiceMock<MockExecutionContext> mockExecutionContext;
AnyExecutionContext ctx{static_cast<MockExecutionContext&>(mockExecutionContext)};
};
@@ -122,7 +125,7 @@ struct AnyExecutionContextTests : Test {
TEST_F(AnyExecutionContextTests, Move)
{
auto mockOp = OperationType<std::any>{};
EXPECT_CALL(mockExecutionContext, execute(An<std::function<std::any()>>())).WillOnce(ReturnRef(mockOp));
EXPECT_CALL(mockExecutionContext, execute(A<std::function<std::any()>>())).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<std::any>{};
EXPECT_CALL(mockExecutionContext, execute(An<std::function<std::any()>>())).WillOnce(ReturnRef(mockOp));
EXPECT_CALL(mockExecutionContext, execute(A<std::function<std::any()>>())).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<std::any>{};
EXPECT_CALL(mockExecutionContext, execute(An<std::function<std::any()>>())).WillOnce(ReturnRef(mockOp));
EXPECT_CALL(mockExecutionContext, execute(A<std::function<std::any()>>())).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<std::any>{};
EXPECT_CALL(mockExecutionContext, execute(An<std::function<std::any()>>()))
EXPECT_CALL(mockExecutionContext, execute(A<std::function<std::any()>>()))
.WillOnce([](auto&&) -> OperationType<std::any> 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<std::any>{};
EXPECT_CALL(mockExecutionContext, execute(An<std::function<std::any(AnyStopToken)>>(), _))
EXPECT_CALL(mockExecutionContext, execute(A<std::function<std::any(AnyStopToken)>>(), _))
.WillOnce(ReturnRef(mockOp));
EXPECT_CALL(mockOp, get());
@@ -175,7 +178,7 @@ TEST_F(AnyExecutionContextTests, ExecuteWithStopTokenAndVoid)
TEST_F(AnyExecutionContextTests, ExecuteWithStopTokenAndVoidThrowsException)
{
EXPECT_CALL(mockExecutionContext, execute(An<std::function<std::any(AnyStopToken)>>(), _))
EXPECT_CALL(mockExecutionContext, execute(A<std::function<std::any(AnyStopToken)>>(), _))
.WillOnce([](auto&&, auto) -> StoppableOperationType<std::any> 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<std::any>{};
EXPECT_CALL(mockOp, get()).WillOnce(Return(std::make_any<int>(42)));
EXPECT_CALL(mockExecutionContext, execute(An<std::function<std::any(AnyStopToken)>>(), _))
EXPECT_CALL(mockExecutionContext, execute(A<std::function<std::any(AnyStopToken)>>(), _))
.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<std::function<std::any(AnyStopToken)>>(), _))
EXPECT_CALL(mockExecutionContext, execute(A<std::function<std::any(AnyStopToken)>>(), _))
.WillOnce([](auto&&, auto) -> StoppableOperationType<std::any> 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<std::any>{};
EXPECT_CALL(mockScheduledOp, cancel());
EXPECT_CALL(
mockExecutionContext,
scheduleAfter(An<std::chrono::milliseconds>(), An<std::function<std::any(AnyStopToken)>>())
mockExecutionContext, scheduleAfter(std::chrono::milliseconds{12}, A<std::function<std::any(AnyStopToken)>>())
)
.WillOnce(ReturnRef(mockScheduledOp));
@@ -223,8 +225,7 @@ TEST_F(AnyExecutionContextTests, TimerExecuted)
auto mockScheduledOp = ScheduledOperationType<std::any>{};
EXPECT_CALL(mockScheduledOp, get()).WillOnce(Return(std::make_any<int>(42)));
EXPECT_CALL(
mockExecutionContext,
scheduleAfter(An<std::chrono::milliseconds>(), An<std::function<std::any(AnyStopToken)>>())
mockExecutionContext, scheduleAfter(std::chrono::milliseconds{12}, A<std::function<std::any(AnyStopToken)>>())
)
.WillOnce([&mockScheduledOp](auto, auto&&) -> ScheduledOperationType<std::any> const& {
return mockScheduledOp;
@@ -242,7 +243,7 @@ TEST_F(AnyExecutionContextTests, TimerWithBoolHandlerCancellation)
EXPECT_CALL(mockScheduledOp, cancel());
EXPECT_CALL(
mockExecutionContext,
scheduleAfter(An<std::chrono::milliseconds>(), An<std::function<std::any(AnyStopToken, bool)>>())
scheduleAfter(std::chrono::milliseconds{12}, A<std::function<std::any(AnyStopToken, bool)>>())
)
.WillOnce(ReturnRef(mockScheduledOp));
@@ -258,7 +259,7 @@ TEST_F(AnyExecutionContextTests, TimerWithBoolHandlerExecuted)
EXPECT_CALL(mockScheduledOp, get()).WillOnce(Return(std::make_any<int>(42)));
EXPECT_CALL(
mockExecutionContext,
scheduleAfter(An<std::chrono::milliseconds>(), An<std::function<std::any(AnyStopToken, bool)>>())
scheduleAfter(std::chrono::milliseconds{12}, A<std::function<std::any(AnyStopToken, bool)>>())
)
.WillOnce([&mockScheduledOp](auto, auto&&) -> ScheduledOperationType<std::any> const& {
return mockScheduledOp;
@@ -270,13 +271,25 @@ TEST_F(AnyExecutionContextTests, TimerWithBoolHandlerExecuted)
EXPECT_EQ(timer.get().value(), 42);
}
TEST_F(AnyExecutionContextTests, RepeatingOperation)
{
auto mockRepeatingOp = RepeatingOperationType<std::any>{};
EXPECT_CALL(mockRepeatingOp, wait());
EXPECT_CALL(mockExecutionContext, executeRepeatedly(std::chrono::milliseconds{1}, A<std::function<std::any()>>()))
.WillOnce([&mockRepeatingOp] -> RepeatingOperationType<std::any> const& { return mockRepeatingOp; });
auto res = ctx.executeRepeatedly(std::chrono::milliseconds{1}, [] -> void { throw 0; });
static_assert(std::is_same_v<decltype(res), AnyOperation<void>>);
res.wait();
}
TEST_F(AnyExecutionContextTests, StrandExecuteWithVoid)
{
auto mockOp = OperationType<std::any>{};
auto mockStrand = StrandType{};
EXPECT_CALL(mockOp, get());
EXPECT_CALL(mockExecutionContext, makeStrand()).WillOnce(ReturnRef(mockStrand));
EXPECT_CALL(mockStrand, execute(An<std::function<std::any()>>())).WillOnce(ReturnRef(mockOp));
EXPECT_CALL(mockStrand, execute(A<std::function<std::any()>>())).WillOnce(ReturnRef(mockOp));
auto strand = ctx.makeStrand();
static_assert(std::is_same_v<decltype(strand), AnyStrand>);
@@ -291,7 +304,7 @@ TEST_F(AnyExecutionContextTests, StrandExecuteWithVoidThrowsException)
{
auto mockStrand = StrandType{};
EXPECT_CALL(mockExecutionContext, makeStrand()).WillOnce(ReturnRef(mockStrand));
EXPECT_CALL(mockStrand, execute(An<std::function<std::any()>>()))
EXPECT_CALL(mockStrand, execute(A<std::function<std::any()>>()))
.WillOnce([](auto&&) -> OperationType<std::any> 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<int>(42)));
EXPECT_CALL(mockExecutionContext, makeStrand()).WillOnce(ReturnRef(mockStrand));
EXPECT_CALL(mockStrand, execute(An<std::function<std::any()>>())).WillOnce(ReturnRef(mockOp));
EXPECT_CALL(mockStrand, execute(A<std::function<std::any()>>())).WillOnce(ReturnRef(mockOp));
auto strand = ctx.makeStrand();
static_assert(std::is_same_v<decltype(strand), AnyStrand>);
@@ -321,7 +334,7 @@ TEST_F(AnyExecutionContextTests, StrandExecuteWithReturnValueThrowsException)
{
auto mockStrand = StrandType{};
EXPECT_CALL(mockExecutionContext, makeStrand()).WillOnce(ReturnRef(mockStrand));
EXPECT_CALL(mockStrand, execute(An<std::function<std::any()>>()))
EXPECT_CALL(mockStrand, execute(A<std::function<std::any()>>()))
.WillOnce([](auto&&) -> OperationType<std::any> 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<std::function<std::any(AnyStopToken)>>(), _)).WillOnce(ReturnRef(mockOp));
EXPECT_CALL(mockStrand, execute(A<std::function<std::any(AnyStopToken)>>(), _)).WillOnce(ReturnRef(mockOp));
auto strand = ctx.makeStrand();
static_assert(std::is_same_v<decltype(strand), AnyStrand>);
@@ -351,7 +364,7 @@ TEST_F(AnyExecutionContextTests, StrandExecuteWithStopTokenAndVoidThrowsExceptio
{
auto mockStrand = StrandType{};
EXPECT_CALL(mockExecutionContext, makeStrand()).WillOnce(ReturnRef(mockStrand));
EXPECT_CALL(mockStrand, execute(An<std::function<std::any(AnyStopToken)>>(), _))
EXPECT_CALL(mockStrand, execute(A<std::function<std::any(AnyStopToken)>>(), _))
.WillOnce([](auto&&, auto) -> StoppableOperationType<std::any> 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<int>(42)));
EXPECT_CALL(mockExecutionContext, makeStrand()).WillOnce(ReturnRef(mockStrand));
EXPECT_CALL(mockStrand, execute(An<std::function<std::any(AnyStopToken)>>(), _)).WillOnce(ReturnRef(mockOp));
EXPECT_CALL(mockStrand, execute(A<std::function<std::any(AnyStopToken)>>(), _)).WillOnce(ReturnRef(mockOp));
auto strand = ctx.makeStrand();
static_assert(std::is_same_v<decltype(strand), AnyStrand>);
@@ -381,7 +394,7 @@ TEST_F(AnyExecutionContextTests, StrandExecuteWithStopTokenAndReturnValueThrowsE
{
auto mockStrand = StrandType{};
EXPECT_CALL(mockExecutionContext, makeStrand()).WillOnce(ReturnRef(mockStrand));
EXPECT_CALL(mockStrand, execute(An<std::function<std::any(AnyStopToken)>>(), _))
EXPECT_CALL(mockStrand, execute(A<std::function<std::any(AnyStopToken)>>(), _))
.WillOnce([](auto&&, auto) -> StoppableOperationType<std::any> const& { throw 0; });
auto strand = ctx.makeStrand();

View File

@@ -36,15 +36,18 @@ struct AnyOperationTests : Test {
using OperationType = MockOperation<std::expected<std::any, ExecutionError>>;
using StoppableOperationType = MockStoppableOperation<std::expected<std::any, ExecutionError>>;
using ScheduledOperationType = MockScheduledOperation<std::expected<std::any, ExecutionError>>;
using RepeatingOperationType = MockRepeatingOperation<std::expected<std::any, ExecutionError>>;
NaggyMock<OperationType> mockOp;
NaggyMock<StoppableOperationType> mockStoppableOp;
NaggyMock<ScheduledOperationType> mockScheduledOp;
NaggyMock<RepeatingOperationType> mockRepeatingOp;
AnyOperation<void> voidOp{impl::ErasedOperation(static_cast<OperationType&>(mockOp))};
AnyOperation<void> voidStoppableOp{impl::ErasedOperation(static_cast<StoppableOperationType&>(mockStoppableOp))};
AnyOperation<int> intOp{impl::ErasedOperation(static_cast<OperationType&>(mockOp))};
AnyOperation<void> scheduledVoidOp{impl::ErasedOperation(static_cast<ScheduledOperationType&>(mockScheduledOp))};
AnyOperation<void> repeatingOp{impl::ErasedOperation(static_cast<RepeatingOperationType&>(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(), ".*");

View File

@@ -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 <gmock/gmock.h>
#include <gtest/gtest.h>
#include <chrono>
#include <cstddef>
#include <semaphore>
#include <stdexcept>
#include <string>
#include <thread>
using namespace util::async;
using ::testing::Types;
@@ -36,6 +41,12 @@ template <typename T>
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();