Skip to content

Commit 81df584

Browse files
authored
Add Clock::sleep_until method (#1748)
* Add Clock::sleep_until method Handle all 3 clock types, and edge cases for shutdown and enabling/disabling ROS time Signed-off-by: Emerson Knapp <eknapp@amazon.com>
1 parent f3a5187 commit 81df584

4 files changed

Lines changed: 300 additions & 2 deletions

File tree

rclcpp/include/rclcpp/clock.hpp

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,25 @@ class Clock
7878
Time
7979
now();
8080

81+
/**
82+
* Sleep until a specified Time, according to clock type.
83+
*
84+
* Notes for RCL_ROS_TIME clock type:
85+
* - Can sleep forever if ros time is active and received clock never reaches `until`
86+
* - If ROS time enabled state changes during the sleep, this method will immediately return
87+
* false. There is not a consistent choice of sleeping time when the time source changes,
88+
* so this is up to the caller to call again if needed.
89+
*
90+
* \param until absolute time according to current clock type to sleep until.
91+
* \return true immediately if `until` is in the past
92+
* \return true when the time `until` is reached
93+
* \return false if time cannot be reached reliably, for example from shutdown or a change
94+
* of time source.
95+
*/
96+
RCLCPP_PUBLIC
97+
bool
98+
sleep_until(Time until);
99+
81100
/**
82101
* Returns the clock of the type `RCL_ROS_TIME` is active.
83102
*

rclcpp/src/rclcpp/clock.cpp

Lines changed: 95 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,13 @@
1414

1515
#include "rclcpp/clock.hpp"
1616

17+
#include <condition_variable>
1718
#include <memory>
1819
#include <thread>
1920

21+
#include "rclcpp/contexts/default_context.hpp"
2022
#include "rclcpp/exceptions.hpp"
23+
#include "rclcpp/utilities.hpp"
2124

2225
#include "rcutils/logging_macros.h"
2326

@@ -47,6 +50,8 @@ class Clock::Impl
4750
rcl_clock_t rcl_clock_;
4851
rcl_allocator_t allocator_;
4952
std::mutex clock_mutex_;
53+
std::condition_variable cv_;
54+
rclcpp::OnShutdownCallbackHandle shutdown_cb_;
5055
};
5156

5257
JumpHandler::JumpHandler(
@@ -59,9 +64,18 @@ JumpHandler::JumpHandler(
5964
{}
6065

6166
Clock::Clock(rcl_clock_type_t clock_type)
62-
: impl_(new Clock::Impl(clock_type)) {}
67+
: impl_(new Clock::Impl(clock_type))
68+
{
69+
impl_->shutdown_cb_ = rclcpp::contexts::get_global_default_context()->add_on_shutdown_callback(
70+
[this]() {
71+
impl_->cv_.notify_all();
72+
});
73+
}
6374

64-
Clock::~Clock() {}
75+
Clock::~Clock()
76+
{
77+
rclcpp::contexts::get_global_default_context()->remove_on_shutdown_callback(impl_->shutdown_cb_);
78+
}
6579

6680
Time
6781
Clock::now()
@@ -76,6 +90,85 @@ Clock::now()
7690
return now;
7791
}
7892

93+
bool
94+
Clock::sleep_until(Time until)
95+
{
96+
const auto this_clock_type = get_clock_type();
97+
if (until.get_clock_type() != this_clock_type) {
98+
RCUTILS_LOG_ERROR(
99+
"sleep_until Time clock type %d does not match this clock's type %d.",
100+
until.get_clock_type(), this_clock_type);
101+
return false;
102+
}
103+
bool time_source_changed = false;
104+
105+
if (this_clock_type == RCL_STEADY_TIME) {
106+
auto steady_time = std::chrono::steady_clock::time_point(
107+
std::chrono::nanoseconds(until.nanoseconds()));
108+
109+
// loop over spurious wakeups but notice shutdown
110+
std::unique_lock lock(impl_->clock_mutex_);
111+
while (now() < until && rclcpp::ok()) {
112+
impl_->cv_.wait_until(lock, steady_time);
113+
}
114+
} else if (this_clock_type == RCL_SYSTEM_TIME) {
115+
auto system_time = std::chrono::time_point<
116+
std::chrono::system_clock, std::chrono::nanoseconds>(
117+
std::chrono::nanoseconds(until.nanoseconds()));
118+
119+
// loop over spurious wakeups but notice shutdown
120+
std::unique_lock lock(impl_->clock_mutex_);
121+
while (now() < until && rclcpp::ok()) {
122+
impl_->cv_.wait_until(lock, system_time);
123+
}
124+
} else if (this_clock_type == RCL_ROS_TIME) {
125+
// Install jump handler for any amount of time change, for two purposes:
126+
// - if ROS time is active, check if time reached on each new clock sample
127+
// - Trigger via on_clock_change to detect if time source changes, to invalidate sleep
128+
rcl_jump_threshold_t threshold;
129+
threshold.on_clock_change = true;
130+
threshold.min_backward.nanoseconds = 0;
131+
threshold.min_forward.nanoseconds = 0;
132+
auto clock_handler = create_jump_callback(
133+
[]() {},
134+
[this](const rcl_time_jump_t &) {impl_->cv_.notify_all();},
135+
threshold);
136+
137+
try {
138+
if (!ros_time_is_active()) {
139+
auto system_time = std::chrono::time_point<
140+
std::chrono::system_clock, std::chrono::nanoseconds>(
141+
std::chrono::nanoseconds(until.nanoseconds()));
142+
143+
// loop over spurious wakeups but notice shutdown or time source change
144+
std::unique_lock lock(impl_->clock_mutex_);
145+
while (now() < until && rclcpp::ok() && !ros_time_is_active()) {
146+
impl_->cv_.wait_until(lock, system_time);
147+
}
148+
time_source_changed = ros_time_is_active();
149+
} else {
150+
// RCL_ROS_TIME with ros_time_is_active.
151+
// Just wait without "until" because installed
152+
// jump callbacks wake the cv on every new sample.
153+
std::unique_lock lock(impl_->clock_mutex_);
154+
while (now() < until && rclcpp::ok() && ros_time_is_active()) {
155+
impl_->cv_.wait(lock);
156+
}
157+
time_source_changed = !ros_time_is_active();
158+
}
159+
} catch (...) {
160+
RCUTILS_LOG_ERROR("Unexpected exception from ros_time_is_active()");
161+
return false;
162+
}
163+
}
164+
165+
if (!rclcpp::ok() || time_source_changed) {
166+
return false;
167+
}
168+
169+
return now() >= until;
170+
}
171+
79172
bool
80173
Clock::ros_time_is_active()
81174
{

rclcpp/test/rclcpp/test_time.cpp

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,15 @@
1717
#include <algorithm>
1818
#include <chrono>
1919
#include <limits>
20+
#include <memory>
2021
#include <string>
2122

2223
#include "rcl/error_handling.h"
2324
#include "rcl/time.h"
2425
#include "rclcpp/clock.hpp"
2526
#include "rclcpp/rclcpp.hpp"
2627
#include "rclcpp/time.hpp"
28+
#include "rclcpp/time_source.hpp"
2729
#include "rclcpp/utilities.hpp"
2830

2931
#include "../utils/rclcpp_gtest_macros.hpp"
@@ -447,3 +449,154 @@ TEST_F(TestTime, test_overflow_underflow_throws) {
447449
test_time = rclcpp::Duration::from_nanoseconds(INT64_MIN) + rclcpp::Time(-1),
448450
std::underflow_error("addition leads to int64_t underflow"));
449451
}
452+
453+
class TestClockSleep : public ::testing::Test
454+
{
455+
protected:
456+
void SetUp()
457+
{
458+
// Shutdown in case there was a dangling global context from other test fixtures
459+
rclcpp::shutdown();
460+
rclcpp::init(0, nullptr);
461+
node = std::make_shared<rclcpp::Node>("clock_sleep_node");
462+
param_client = std::make_shared<rclcpp::SyncParametersClient>(node);
463+
ASSERT_TRUE(param_client->wait_for_service(5s));
464+
}
465+
466+
void TearDown()
467+
{
468+
node.reset();
469+
rclcpp::shutdown();
470+
}
471+
472+
rclcpp::Node::SharedPtr node;
473+
rclcpp::SyncParametersClient::SharedPtr param_client;
474+
};
475+
476+
TEST_F(TestClockSleep, bad_clock_type) {
477+
rclcpp::Clock clock(RCL_SYSTEM_TIME);
478+
rclcpp::Time steady_until(12345, 0, RCL_STEADY_TIME);
479+
ASSERT_FALSE(clock.sleep_until(steady_until));
480+
481+
rclcpp::Time ros_until(54321, 0, RCL_ROS_TIME);
482+
ASSERT_FALSE(clock.sleep_until(ros_until));
483+
}
484+
485+
TEST_F(TestClockSleep, sleep_until_basic_system) {
486+
static const auto MILLION = 1000L * 1000L;
487+
const auto milliseconds = 300;
488+
rclcpp::Clock clock(RCL_SYSTEM_TIME);
489+
auto delay = rclcpp::Duration(0, milliseconds * MILLION);
490+
auto sleep_until = clock.now() + delay;
491+
492+
auto start = std::chrono::system_clock::now();
493+
ASSERT_TRUE(clock.sleep_until(sleep_until));
494+
auto end = std::chrono::system_clock::now();
495+
496+
EXPECT_GE(clock.now(), sleep_until);
497+
EXPECT_GE(end - start, std::chrono::milliseconds(milliseconds));
498+
}
499+
500+
TEST_F(TestClockSleep, sleep_until_basic_steady) {
501+
static const auto MILLION = 1000L * 1000L;
502+
const auto milliseconds = 300;
503+
rclcpp::Clock clock(RCL_STEADY_TIME);
504+
auto delay = rclcpp::Duration(0, milliseconds * MILLION);
505+
auto sleep_until = clock.now() + delay;
506+
507+
auto steady_start = std::chrono::steady_clock::now();
508+
ASSERT_TRUE(clock.sleep_until(sleep_until));
509+
auto steady_end = std::chrono::steady_clock::now();
510+
511+
EXPECT_GE(clock.now(), sleep_until);
512+
EXPECT_GE(steady_end - steady_start, std::chrono::milliseconds(milliseconds));
513+
}
514+
515+
TEST_F(TestClockSleep, sleep_until_steady_past_returns_immediately) {
516+
rclcpp::Clock clock(RCL_STEADY_TIME);
517+
auto until = clock.now() - rclcpp::Duration(1000, 0);
518+
// This should return immediately, other possible behavior might be sleep forever and timeout
519+
ASSERT_TRUE(clock.sleep_until(until));
520+
}
521+
522+
TEST_F(TestClockSleep, sleep_until_system_past_returns_immediately) {
523+
rclcpp::Clock clock(RCL_SYSTEM_TIME);
524+
auto until = clock.now() - rclcpp::Duration(1000, 0);
525+
// This should return immediately, other possible behavior might be sleep forever and timeout
526+
ASSERT_TRUE(clock.sleep_until(until));
527+
}
528+
529+
TEST_F(TestClockSleep, sleep_until_ros_time_enable_interrupt) {
530+
auto clock = std::make_shared<rclcpp::Clock>(RCL_ROS_TIME);
531+
rclcpp::TimeSource time_source;
532+
time_source.attachNode(node);
533+
time_source.attachClock(clock);
534+
535+
// 5 second timeout, but it should be interrupted right away
536+
const auto until = clock->now() + rclcpp::Duration(5, 0);
537+
538+
// Try sleeping with ROS time off, then turn it on to interrupt
539+
bool sleep_succeeded = true;
540+
auto sleep_thread = std::thread(
541+
[clock, until, &sleep_succeeded]() {
542+
sleep_succeeded = clock->sleep_until(until);
543+
});
544+
// yield execution long enough to let the sleep thread get to waiting on the condition variable
545+
std::this_thread::sleep_for(std::chrono::milliseconds(200));
546+
auto set_parameters_results = param_client->set_parameters(
547+
{rclcpp::Parameter("use_sim_time", true)});
548+
for (auto & result : set_parameters_results) {
549+
ASSERT_TRUE(result.successful);
550+
}
551+
sleep_thread.join();
552+
EXPECT_FALSE(sleep_succeeded);
553+
}
554+
555+
TEST_F(TestClockSleep, sleep_until_ros_time_disable_interrupt) {
556+
param_client->set_parameters({rclcpp::Parameter("use_sim_time", true)});
557+
auto clock = std::make_shared<rclcpp::Clock>(RCL_ROS_TIME);
558+
rclcpp::TimeSource time_source;
559+
time_source.attachNode(node);
560+
time_source.attachClock(clock);
561+
562+
// /clock shouldn't be publishing, shouldn't be possible to reach timeout
563+
const auto until = clock->now() + rclcpp::Duration(600, 0);
564+
565+
// Try sleeping with ROS time off, then turn it on to interrupt
566+
bool sleep_succeeded = true;
567+
auto sleep_thread = std::thread(
568+
[clock, until, &sleep_succeeded]() {
569+
sleep_succeeded = clock->sleep_until(until);
570+
});
571+
// yield execution long enough to let the sleep thread get to waiting on the condition variable
572+
std::this_thread::sleep_for(std::chrono::milliseconds(200));
573+
auto set_parameters_results = param_client->set_parameters(
574+
{rclcpp::Parameter("use_sim_time", false)});
575+
for (auto & result : set_parameters_results) {
576+
ASSERT_TRUE(result.successful);
577+
}
578+
sleep_thread.join();
579+
EXPECT_FALSE(sleep_succeeded);
580+
}
581+
582+
TEST_F(TestClockSleep, sleep_until_shutdown_interrupt) {
583+
param_client->set_parameters({rclcpp::Parameter("use_sim_time", true)});
584+
auto clock = std::make_shared<rclcpp::Clock>(RCL_ROS_TIME);
585+
rclcpp::TimeSource time_source;
586+
time_source.attachNode(node);
587+
time_source.attachClock(clock);
588+
589+
// the timeout doesn't matter here - no /clock is being published, so it should never wake
590+
const auto until = clock->now() + rclcpp::Duration(600, 0);
591+
592+
bool sleep_succeeded = true;
593+
auto sleep_thread = std::thread(
594+
[clock, until, &sleep_succeeded]() {
595+
sleep_succeeded = clock->sleep_until(until);
596+
});
597+
// yield execution long enough to let the sleep thread get to waiting on the condition variable
598+
std::this_thread::sleep_for(std::chrono::milliseconds(200));
599+
rclcpp::shutdown();
600+
sleep_thread.join();
601+
EXPECT_FALSE(sleep_succeeded);
602+
}

rclcpp/test/rclcpp/test_time_source.cpp

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -742,3 +742,36 @@ TEST_F(TestTimeSource, check_sim_time_updated_in_callback_if_use_clock_thread) {
742742
// Node should have get out of timer callback
743743
ASSERT_FALSE(clock_thread_testing_node.GetIsCallbackFrozen());
744744
}
745+
746+
TEST_F(TestTimeSource, clock_sleep_until_with_ros_time_basic) {
747+
SimClockPublisherNode pub_node;
748+
pub_node.SpinNode();
749+
750+
node->set_parameter({"use_sim_time", true});
751+
auto clock = std::make_shared<rclcpp::Clock>(RCL_ROS_TIME);
752+
rclcpp::TimeSource time_source;
753+
time_source.attachNode(node);
754+
time_source.attachClock(clock);
755+
756+
// Wait until time source has definitely received a first ROS time from the pub node
757+
{
758+
rcl_jump_threshold_t threshold;
759+
threshold.on_clock_change = false;
760+
threshold.min_backward.nanoseconds = 0;
761+
threshold.min_forward.nanoseconds = 0;
762+
763+
std::condition_variable cv;
764+
std::mutex mutex;
765+
auto handler = clock->create_jump_callback(
766+
[]() {},
767+
[&cv](const rcl_time_jump_t &) {cv.notify_all();},
768+
threshold);
769+
std::unique_lock lock(mutex);
770+
cv.wait(lock);
771+
}
772+
773+
auto now = clock->now();
774+
// Any amount of time will do, just need to make sure that we awake and return true
775+
auto until = now + rclcpp::Duration(0, 500);
776+
EXPECT_TRUE(clock->sleep_until(until));
777+
}

0 commit comments

Comments
 (0)