summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--example/Makefile.am4
-rw-r--r--example/delayed_echo.cpp59
-rw-r--r--src/sdeventplus/utility/timer.cpp51
-rw-r--r--src/sdeventplus/utility/timer.hpp44
-rw-r--r--test/utility/timer.cpp182
6 files changed, 303 insertions, 38 deletions
diff --git a/.gitignore b/.gitignore
index d9423e4..5353da2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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
OpenPOWER on IntegriCloud