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>
diff --git a/.gitignore b/.gitignore
index d9423e4..5353da2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -41,6 +41,7 @@
/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 @@
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 @@
{
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 @@
}
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 @@
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 @@
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 @@
}
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 @@
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 @@
/** @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 @@
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 @@
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 @@
* 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 @@
*
* @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 @@
/** @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 @@
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 @@
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 @@
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 @@
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 @@
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, 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 @@
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