diff options
| author | William A. Kennington III <wak@google.com> | 2018-09-20 11:35:11 -0700 |
|---|---|---|
| committer | William A. Kennington III <wak@google.com> | 2018-09-26 12:18:50 -0700 |
| commit | ba04ffb5a94fd47d8acc57c7a6be4d31624d2c23 (patch) | |
| tree | aa8629e7418795879a563ff9dc6909985b79899e | |
| parent | fa9431d52f37bf51a2aa0d3a85eaaf027db8488f (diff) | |
| download | sdeventplus-ba04ffb5a94fd47d8acc57c7a6be4d31624d2c23.tar.gz sdeventplus-ba04ffb5a94fd47d8acc57c7a6be4d31624d2c23.zip | |
utility/timer: Implement oneshot timers
This change is meant to enable users of the old openbmc timer class to
trivially use the timer if they only want single executions. It also
makes setting up the timer less verbose if you do not already know the
timeout interval.
Tested:
Run through unit tests and did a sample integration with
phosphor-watchdog and phosphor-networkd. Verified that the new
oneshot example works as expected.
Change-Id: I2cd006d1f19fff99bce3f732a16eac9ca9553666
Signed-off-by: William A. Kennington III <wak@google.com>
| -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 |

