fix: Improve Repeat implementation (#1775)

This commit is contained in:
Sergey Kuznetsov
2024-12-11 15:07:48 +00:00
committed by GitHub
parent c41399ef8e
commit b53cfd0ec1
3 changed files with 41 additions and 14 deletions

View File

@@ -27,12 +27,16 @@ Repeat::Repeat(boost::asio::io_context& ioc) : timer_(ioc)
{ {
} }
Repeat::~Repeat()
{
*stopped_ = true;
}
void void
Repeat::stop() Repeat::stop()
{ {
stopping_ = true; *stopped_ = true;
timer_.cancel(); timer_.cancel();
semaphore_.acquire();
} }
} // namespace util } // namespace util

View File

@@ -19,6 +19,8 @@
#pragma once #pragma once
#include "util/Assert.hpp"
#include <boost/asio/io_context.hpp> #include <boost/asio/io_context.hpp>
#include <boost/asio/post.hpp> #include <boost/asio/post.hpp>
#include <boost/asio/steady_timer.hpp> #include <boost/asio/steady_timer.hpp>
@@ -26,7 +28,7 @@
#include <atomic> #include <atomic>
#include <chrono> #include <chrono>
#include <concepts> #include <concepts>
#include <semaphore> #include <memory>
namespace util { namespace util {
@@ -36,8 +38,7 @@ namespace util {
*/ */
class Repeat { class Repeat {
boost::asio::steady_timer timer_; boost::asio::steady_timer timer_;
std::atomic_bool stopping_{false}; std::shared_ptr<std::atomic_bool> stopped_ = std::make_shared<std::atomic_bool>(true);
std::binary_semaphore semaphore_{0};
public: public:
/** /**
@@ -47,6 +48,11 @@ public:
*/ */
Repeat(boost::asio::io_context& ioc); Repeat(boost::asio::io_context& ioc);
/**
* @brief Destroy the Repeat object
*/
~Repeat();
/** /**
* @brief Stop repeating * @brief Stop repeating
* @note This method will block to ensure the repeating is actually stopped. But blocking time should be very short. * @note This method will block to ensure the repeating is actually stopped. But blocking time should be very short.
@@ -56,6 +62,7 @@ public:
/** /**
* @brief Start asynchronously repeating * @brief Start asynchronously repeating
* @note stop() must be called before start() is called for the second time
* *
* @tparam Action The action type * @tparam Action The action type
* @param interval The interval to repeat * @param interval The interval to repeat
@@ -65,7 +72,9 @@ public:
void void
start(std::chrono::steady_clock::duration interval, Action&& action) start(std::chrono::steady_clock::duration interval, Action&& action)
{ {
stopping_ = false; 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<std::atomic_bool>(false);
startImpl(interval, std::forward<Action>(action)); startImpl(interval, std::forward<Action>(action));
} }
@@ -75,9 +84,10 @@ private:
startImpl(std::chrono::steady_clock::duration interval, Action&& action) startImpl(std::chrono::steady_clock::duration interval, Action&& action)
{ {
timer_.expires_after(interval); timer_.expires_after(interval);
timer_.async_wait([this, interval, action = std::forward<Action>(action)](auto const&) mutable { timer_.async_wait([this, interval, stopping = stopped_, action = std::forward<Action>(action)](
if (stopping_) { auto const& errorCode
semaphore_.release(); ) mutable {
if (errorCode or *stopping) {
return; return;
} }
action(); action();

View File

@@ -34,7 +34,7 @@
using namespace util; using namespace util;
using testing::AtLeast; using testing::AtLeast;
struct RepeatTests : SyncAsioContextTest { struct RepeatTest : SyncAsioContextTest {
Repeat repeat{ctx}; Repeat repeat{ctx};
testing::StrictMock<testing::MockFunction<void()>> handlerMock; testing::StrictMock<testing::MockFunction<void()>> handlerMock;
@@ -51,14 +51,14 @@ struct RepeatTests : SyncAsioContextTest {
} }
}; };
TEST_F(RepeatTests, CallsHandler) TEST_F(RepeatTest, CallsHandler)
{ {
repeat.start(std::chrono::milliseconds{1}, handlerMock.AsStdFunction()); repeat.start(std::chrono::milliseconds{1}, handlerMock.AsStdFunction());
EXPECT_CALL(handlerMock, Call).Times(AtLeast(10)); EXPECT_CALL(handlerMock, Call).Times(AtLeast(10));
runContextFor(std::chrono::milliseconds{20}); runContextFor(std::chrono::milliseconds{20});
} }
TEST_F(RepeatTests, StopsOnStop) TEST_F(RepeatTest, StopsOnStop)
{ {
withRunningContext([this]() { withRunningContext([this]() {
repeat.start(std::chrono::milliseconds{1}, handlerMock.AsStdFunction()); repeat.start(std::chrono::milliseconds{1}, handlerMock.AsStdFunction());
@@ -68,10 +68,10 @@ TEST_F(RepeatTests, StopsOnStop)
}); });
} }
TEST_F(RepeatTests, RunsAfterStop) TEST_F(RepeatTest, RunsAfterStop)
{ {
withRunningContext([this]() { withRunningContext([this]() {
for ([[maybe_unused]] auto _ : std::ranges::iota_view(0, 2)) { for ([[maybe_unused]] auto i : std::ranges::iota_view(0, 2)) {
repeat.start(std::chrono::milliseconds{1}, handlerMock.AsStdFunction()); repeat.start(std::chrono::milliseconds{1}, handlerMock.AsStdFunction());
EXPECT_CALL(handlerMock, Call).Times(AtLeast(1)); EXPECT_CALL(handlerMock, Call).Times(AtLeast(1));
std::this_thread::sleep_for(std::chrono::milliseconds{10}); std::this_thread::sleep_for(std::chrono::milliseconds{10});
@@ -79,3 +79,16 @@ TEST_F(RepeatTests, RunsAfterStop)
} }
}); });
} }
struct RepeatDeathTest : RepeatTest {};
TEST_F(RepeatDeathTest, DiesWhenStartCalledTwice)
{
EXPECT_DEATH(
{
repeat.start(std::chrono::seconds{1}, []() {});
repeat.start(std::chrono::seconds{1}, []() {});
},
"Assertion .* failed.*"
);
}