diff options
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | example/Makefile.am | 4 | ||||
| -rw-r--r-- | example/delayed_echo.cpp | 59 | ||||
| -rw-r--r-- | src/sdeventplus/utility/timer.cpp | 51 | ||||
| -rw-r--r-- | src/sdeventplus/utility/timer.hpp | 44 | ||||
| -rw-r--r-- | test/utility/timer.cpp | 182 |
6 files changed, 303 insertions, 38 deletions
@@ -41,6 +41,7 @@ Makefile.in /src/sdeventplus.pc # Output binaries +/example/delayed_echo /example/follow /example/heartbeat /example/heartbeat_timer diff --git a/example/Makefile.am b/example/Makefile.am index 96f2faa..fcdbaa5 100644 --- a/example/Makefile.am +++ b/example/Makefile.am @@ -2,6 +2,10 @@ noinst_PROGRAMS = if BUILD_EXAMPLES +noinst_PROGRAMS += delayed_echo +delayed_echo_SOURCES = delayed_echo.cpp +delayed_echo_LDADD = $(SDEVENTPLUS_LIBS) + noinst_PROGRAMS += follow follow_SOURCES = follow.cpp follow_LDADD = $(SDEVENTPLUS_LIBS) diff --git a/example/delayed_echo.cpp b/example/delayed_echo.cpp new file mode 100644 index 0000000..e7fc33b --- /dev/null +++ b/example/delayed_echo.cpp @@ -0,0 +1,59 @@ +/** + * Reads stdin looking for a string, and coalesces that buffer until stdin + * is calm for the passed in number of seconds. + */ + +#include <array> +#include <chrono> +#include <cstdio> +#include <sdeventplus/clock.hpp> +#include <sdeventplus/event.hpp> +#include <sdeventplus/source/io.hpp> +#include <sdeventplus/utility/timer.hpp> +#include <string> +#include <unistd.h> +#include <utility> + +using sdeventplus::Clock; +using sdeventplus::ClockId; +using sdeventplus::Event; +using sdeventplus::source::IO; + +constexpr auto clockId = ClockId::RealTime; +using Timer = sdeventplus::utility::Timer<clockId>; + +int main(int argc, char* argv[]) +{ + if (argc != 2) + { + fprintf(stderr, "Usage: %s [seconds]\n", argv[0]); + return 1; + } + + std::chrono::seconds delay(std::stoul(argv[1])); + + auto event = Event::get_default(); + + std::string content; + auto timerCb = [&](Timer&) { + printf("%s", content.c_str()); + content.clear(); + }; + Timer timer(event, std::move(timerCb)); + + auto ioCb = [&](IO&, int fd, uint32_t) { + std::array<char, 4096> buffer; + ssize_t bytes = read(fd, buffer.data(), buffer.size()); + if (bytes <= 0) + { + printf("%s", content.c_str()); + event.exit(bytes < 0); + return; + } + content.append(buffer.data(), bytes); + timer.restartOnce(delay); + }; + IO ioSource(event, STDIN_FILENO, EPOLLIN, std::move(ioCb)); + + return event.loop(); +} diff --git a/src/sdeventplus/utility/timer.cpp b/src/sdeventplus/utility/timer.cpp index 7c75e07..dfe4ca7 100644 --- a/src/sdeventplus/utility/timer.cpp +++ b/src/sdeventplus/utility/timer.cpp @@ -9,15 +9,18 @@ namespace utility { template <ClockId Id> -Timer<Id>::Timer(const Event& event, Callback&& callback, Duration interval, +Timer<Id>::Timer(const Event& event, Callback&& callback, + std::optional<Duration> interval, typename source::Time<Id>::Accuracy accuracy) : expired(false), - callback(callback), clock(event), interval(interval), - timeSource(event, clock.now() + interval, accuracy, + initialized(interval.has_value()), callback(callback), clock(event), + interval(interval), + timeSource(event, clock.now() + interval.value_or(Duration::zero()), + accuracy, std::bind(&Timer::internalCallback, this, std::placeholders::_1, std::placeholders::_2)) { - timeSource.set_enabled(source::Enabled::On); + setEnabled(interval.has_value()); } template <ClockId Id> @@ -33,7 +36,7 @@ bool Timer<Id>::isEnabled() const } template <ClockId Id> -typename Timer<Id>::Duration Timer<Id>::getInterval() const +std::optional<typename Timer<Id>::Duration> Timer<Id>::getInterval() const { return interval; } @@ -58,6 +61,10 @@ typename Timer<Id>::Duration Timer<Id>::getRemaining() const template <ClockId Id> void Timer<Id>::setEnabled(bool enabled) { + if (enabled && !initialized) + { + throw std::runtime_error("Timer was never initialized"); + } timeSource.set_enabled(enabled ? source::Enabled::On : source::Enabled::Off); } @@ -66,16 +73,17 @@ template <ClockId Id> void Timer<Id>::setRemaining(Duration remaining) { timeSource.set_time(clock.now() + remaining); + initialized = true; } template <ClockId Id> void Timer<Id>::resetRemaining() { - setRemaining(interval); + setRemaining(interval.value()); } template <ClockId Id> -void Timer<Id>::setInterval(Duration interval) +void Timer<Id>::setInterval(std::optional<Duration> interval) { this->interval = interval; } @@ -87,11 +95,25 @@ void Timer<Id>::clearExpired() } template <ClockId Id> -void Timer<Id>::restart(Duration interval) +void Timer<Id>::restart(std::optional<Duration> interval) { clearExpired(); + initialized = false; setInterval(interval); - resetRemaining(); + if (interval) + { + resetRemaining(); + } + setEnabled(interval.has_value()); +} + +template <ClockId Id> +void Timer<Id>::restartOnce(Duration remaining) +{ + clearExpired(); + initialized = false; + setInterval(std::nullopt); + setRemaining(remaining); setEnabled(true); } @@ -100,11 +122,20 @@ void Timer<Id>::internalCallback(source::Time<Id>&, typename source::Time<Id>::TimePoint) { expired = true; + initialized = false; + if (interval) + { + resetRemaining(); + } + else + { + setEnabled(false); + } + if (callback) { callback(*this); } - resetRemaining(); } template class Timer<ClockId::RealTime>; diff --git a/src/sdeventplus/utility/timer.hpp b/src/sdeventplus/utility/timer.hpp index 96c5b6b..301842b 100644 --- a/src/sdeventplus/utility/timer.hpp +++ b/src/sdeventplus/utility/timer.hpp @@ -2,6 +2,7 @@ #include <chrono> #include <functional> +#include <optional> #include <sdeventplus/clock.hpp> #include <sdeventplus/event.hpp> #include <sdeventplus/source/time.hpp> @@ -14,12 +15,13 @@ namespace utility /** @class Timer<Id> * @brief A simple, repeating timer around an sd_event time source * @details Adds a timer to the SdEvent loop that runs a user defined callback - * at specified intervals. Besides running callbacks, the timer tracks - * whether or not it has expired since creation or since the last - * clearExpired() or restart(). The concept of expiration is + * at specified intervals. If no interval is provided to the timer, + * it can be used for oneshot actions. Besides running callbacks, the + * timer tracks whether or not it has expired since creation or since + * the last clearExpired() or restart(). The concept of expiration is * orthogonal to the callback mechanism and can be ignored. * - * See example/heartbeat_timer.cpp for usage examples. + * See example/{heartbeat_timer,delayed_echo}.cpp for usage examples. */ template <ClockId Id> class Timer @@ -42,17 +44,20 @@ class Timer virtual ~Timer() = default; /** @brief Creates a new timer on the given event loop. - * This timer is created enabled by default. + * This timer is created enabled by default if passed an interval. * * @param[in] event - The event we are attaching to * @param[in] callback - The user provided callback run when elapsing * This can be empty - * @param[in] interval - The amount of time in between timer expirations + * @param[in] interval - Optional amount of time in-between timer + * expirations. std::nullopt means the interval + * will be provided later. * @param[in] accuracy - Optional amount of error tolerable in timer * expiration. Defaults to 1ms. * @throws SdEventError for underlying sd_event errors */ - Timer(const Event& event, Callback&& callback, Duration interval, + Timer(const Event& event, Callback&& callback, + std::optional<Duration> interval = std::nullopt, typename source::Time<Id>::Accuracy accuracy = std::chrono::milliseconds{1}); @@ -71,10 +76,12 @@ class Timer bool isEnabled() const; /** @brief Gets interval between timer expirations + * The timer may not have a configured interval and is instead + * operating as a one-shot timer. * * @return The interval as an std::chrono::duration */ - Duration getInterval() const; + std::optional<Duration> getInterval() const; /** @brief Gets time left before the timer expirations * @@ -88,6 +95,7 @@ class Timer * This does not alter the expiration time of the timer. * * @param[in] enabled - Should the timer be enabled or disabled + * @throws std::runtime_error If the timer has not been initialized * @throws SdEventError for underlying sd_event errors */ void setEnabled(bool enabled); @@ -111,7 +119,7 @@ class Timer * * @param[in] interval - The new interval for the timer */ - void setInterval(Duration interval); + void setInterval(std::optional<Duration> interval); /** @brief Resets the expired status of the timer. */ void clearExpired(); @@ -119,22 +127,34 @@ class Timer /** @brief Restarts the timer as though it has been completely * re-initialized. Expired status is reset, interval is updated, * time remaining is set to the new interval, and the timer is - * enabled. + * enabled if the interval is populated. * * @param[in] interval - The new interval for the timer * @throws SdEventError for underlying sd_event errors */ - void restart(Duration interval); + void restart(std::optional<Duration> interval); + + /** @brief Restarts the timer as though it has been completely + * re-initialized. Expired status is reset, interval is removed, + * time remaining is set to the new remaining, and the timer is + * enabled as a one shot. + * + * @param[in] interval - The new interval for the timer + * @throws SdEventError for underlying sd_event errors + */ + void restartOnce(Duration remaining); private: /** @brief Tracks the expiration status of the timer */ bool expired; + /** @brief Tracks whether or not the expiration timeout is valid */ + bool initialized; /** @brief User defined callback run on each expiration */ Callback callback; /** @brief Clock used for updating the time source */ Clock<Id> clock; /** @brief Interval between each timer expiration */ - Duration interval; + std::optional<Duration> interval; /** @brief Underlying sd_event time source that backs the timer */ source::Time<Id> timeSource; diff --git a/test/utility/timer.cpp b/test/utility/timer.cpp index 414ad82..a329287 100644 --- a/test/utility/timer.cpp +++ b/test/utility/timer.cpp @@ -2,6 +2,7 @@ #include <gmock/gmock.h> #include <gtest/gtest.h> #include <memory> +#include <optional> #include <sdeventplus/clock.hpp> #include <sdeventplus/event.hpp> #include <sdeventplus/test/sdevent.hpp> @@ -50,6 +51,7 @@ class TimerTest : public testing::Test const milliseconds starting_time{10}; sd_event_time_handler_t handler = nullptr; void* handler_userdata; + std::unique_ptr<Event> event; std::unique_ptr<TestTimer> timer; std::function<void()> callback; @@ -83,14 +85,41 @@ class TimerTest : public testing::Test DoAll(SetArgPointee<1>(static_cast<int>(enabled)), Return(0))); } + void resetTimer() + { + if (timer) + { + expectSetEnabled(source::Enabled::Off); + timer.reset(); + } + } + + void expireTimer() + { + const milliseconds new_time(90); + expectNow(new_time); + expectSetTime(new_time + interval); + EXPECT_EQ(0, handler(nullptr, 0, handler_userdata)); + EXPECT_TRUE(timer->hasExpired()); + EXPECT_EQ(interval, timer->getInterval()); + } + void SetUp() { EXPECT_CALL(mock, sd_event_ref(expected_event)) .WillRepeatedly(DoAll(EventRef(), Return(expected_event))); EXPECT_CALL(mock, sd_event_unref(expected_event)) .WillRepeatedly(DoAll(EventUnref(), Return(nullptr))); - Event event(expected_event, &mock); + event = std::make_unique<Event>(expected_event, &mock); + EXPECT_CALL(mock, sd_event_source_unref(expected_source)) + .WillRepeatedly(Return(nullptr)); + EXPECT_CALL(mock, + sd_event_source_set_userdata(expected_source, testing::_)) + .WillRepeatedly( + DoAll(SaveArg<1>(&handler_userdata), Return(nullptr))); + // Having a callback proxy allows us to update the test callback + // dynamically, without changing it inside the timer auto runCallback = [&](TestTimer&) { if (callback) { @@ -105,24 +134,54 @@ class TimerTest : public testing::Test 1000, testing::_, nullptr)) .WillOnce(DoAll(SetArgPointee<1>(expected_source), SaveArg<5>(&handler), Return(0))); - EXPECT_CALL(mock, - sd_event_source_set_userdata(expected_source, testing::_)) - .WillOnce(DoAll(SaveArg<1>(&handler_userdata), Return(nullptr))); - // Timer always enables the source to keep ticking expectSetEnabled(source::Enabled::On); - timer = std::make_unique<TestTimer>(event, runCallback, interval); + timer = std::make_unique<TestTimer>(*event, runCallback, interval); } void TearDown() { - expectSetEnabled(source::Enabled::Off); - EXPECT_CALL(mock, sd_event_source_unref(expected_source)) - .WillOnce(Return(nullptr)); - timer.reset(); + resetTimer(); + event.reset(); EXPECT_EQ(0, event_ref_times); } }; +TEST_F(TimerTest, NoCallback) +{ + resetTimer(); + expectNow(starting_time); + EXPECT_CALL( + mock, sd_event_add_time(expected_event, testing::_, + static_cast<clockid_t>(testClock), + microseconds(starting_time + interval).count(), + 1000, testing::_, nullptr)) + .WillOnce(DoAll(SetArgPointee<1>(expected_source), SaveArg<5>(&handler), + Return(0))); + expectSetEnabled(source::Enabled::On); + timer = std::make_unique<TestTimer>(*event, nullptr, interval); + + expectNow(starting_time); + expectSetTime(starting_time + interval); + EXPECT_EQ(0, handler(nullptr, 0, handler_userdata)); +} + +TEST_F(TimerTest, NoInterval) +{ + resetTimer(); + expectNow(starting_time); + EXPECT_CALL(mock, sd_event_add_time(expected_event, testing::_, + static_cast<clockid_t>(testClock), + microseconds(starting_time).count(), + 1000, testing::_, nullptr)) + .WillOnce(DoAll(SetArgPointee<1>(expected_source), SaveArg<5>(&handler), + Return(0))); + expectSetEnabled(source::Enabled::Off); + timer = std::make_unique<TestTimer>(*event, nullptr); + + EXPECT_EQ(std::nullopt, timer->getInterval()); + EXPECT_THROW(timer->setEnabled(true), std::runtime_error); +} + TEST_F(TimerTest, NewTimer) { EXPECT_FALSE(timer->hasExpired()); @@ -184,6 +243,32 @@ TEST_F(TimerTest, SetEnabled) EXPECT_FALSE(timer->hasExpired()); } +TEST_F(TimerTest, SetEnabledUnsetTimer) +{ + // Force the timer to become unset + expectSetEnabled(source::Enabled::Off); + timer->restart(std::nullopt); + + // Setting an interval should not update the timer directly + timer->setInterval(milliseconds(90)); + + expectSetEnabled(source::Enabled::Off); + timer->setEnabled(false); + EXPECT_THROW(timer->setEnabled(true), std::runtime_error); +} + +TEST_F(TimerTest, SetEnabledOneshot) +{ + // Timer effectively becomes oneshot if it gets initialized but has + // the interval removed + timer->setInterval(std::nullopt); + + expectSetEnabled(source::Enabled::Off); + timer->setEnabled(false); + expectSetEnabled(source::Enabled::On); + timer->setEnabled(true); +} + TEST_F(TimerTest, SetRemaining) { const milliseconds now(90), remaining(30); @@ -212,6 +297,48 @@ TEST_F(TimerTest, SetInterval) EXPECT_FALSE(timer->hasExpired()); } +TEST_F(TimerTest, SetIntervalEmpty) +{ + timer->setInterval(std::nullopt); + EXPECT_EQ(std::nullopt, timer->getInterval()); + EXPECT_FALSE(timer->hasExpired()); +} + +TEST_F(TimerTest, CallbackHappensLast) +{ + const milliseconds new_time(90); + expectNow(new_time); + expectSetTime(new_time + interval); + callback = [&]() { + EXPECT_TRUE(timer->hasExpired()); + expectSetEnabled(source::Enabled::On); + timer->setEnabled(true); + timer->clearExpired(); + timer->setInterval(std::nullopt); + }; + EXPECT_EQ(0, handler(nullptr, 0, handler_userdata)); + EXPECT_FALSE(timer->hasExpired()); + EXPECT_EQ(std::nullopt, timer->getInterval()); + expectSetEnabled(source::Enabled::On); + timer->setEnabled(true); +} + +TEST_F(TimerTest, CallbackOneshot) +{ + // Make sure we try a one shot so we can test the callback + // correctly + timer->setInterval(std::nullopt); + + expectSetEnabled(source::Enabled::Off); + callback = [&]() { + EXPECT_TRUE(timer->hasExpired()); + EXPECT_THROW(timer->setEnabled(true), std::runtime_error); + timer->setInterval(interval); + }; + EXPECT_EQ(0, handler(nullptr, 0, handler_userdata)); + EXPECT_THROW(timer->setEnabled(true), std::runtime_error); +} + TEST_F(TimerTest, SetValuesExpiredTimer) { const milliseconds new_time(90); @@ -242,12 +369,7 @@ TEST_F(TimerTest, SetValuesExpiredTimer) TEST_F(TimerTest, Restart) { - const milliseconds new_time(90); - expectNow(new_time); - expectSetTime(new_time + interval); - EXPECT_EQ(0, handler(nullptr, 0, handler_userdata)); - EXPECT_TRUE(timer->hasExpired()); - EXPECT_EQ(interval, timer->getInterval()); + expireTimer(); const milliseconds new_interval(471); expectNow(starting_time); @@ -256,6 +378,34 @@ TEST_F(TimerTest, Restart) timer->restart(new_interval); EXPECT_FALSE(timer->hasExpired()); EXPECT_EQ(new_interval, timer->getInterval()); + expectSetEnabled(source::Enabled::On); + timer->setEnabled(true); +} + +TEST_F(TimerTest, RestartEmpty) +{ + expireTimer(); + + expectSetEnabled(source::Enabled::Off); + timer->restart(std::nullopt); + EXPECT_FALSE(timer->hasExpired()); + EXPECT_EQ(std::nullopt, timer->getInterval()); + EXPECT_THROW(timer->setEnabled(true), std::runtime_error); +} + +TEST_F(TimerTest, RestartOnce) +{ + expireTimer(); + + const milliseconds remaining(471); + expectNow(starting_time); + expectSetTime(starting_time + remaining); + expectSetEnabled(source::Enabled::On); + timer->restartOnce(remaining); + EXPECT_FALSE(timer->hasExpired()); + EXPECT_EQ(std::nullopt, timer->getInterval()); + expectSetEnabled(source::Enabled::On); + timer->setEnabled(true); } } // namespace |

