Skip to content

Commit 5a6122a

Browse files
committed
Add support for spin_until_timeout (ros2#1821)
* Created spin_until_timeout() method * Created spin_node_until_timeout() method * Extended unit tests Signed-off-by: Hubert Liberacki <[email protected]>
1 parent d3c0049 commit 5a6122a

File tree

3 files changed

+201
-5
lines changed

3 files changed

+201
-5
lines changed

rclcpp/include/rclcpp/executor.hpp

+46-3
Original file line numberDiff line numberDiff line change
@@ -344,16 +344,18 @@ class Executor
344344
}
345345

346346
auto end_time = std::chrono::steady_clock::now();
347+
348+
if (spinning.exchange(true)) {
349+
throw std::runtime_error("spin_until_future_complete() called while already spinning");
350+
}
351+
347352
std::chrono::nanoseconds timeout_ns = std::chrono::duration_cast<std::chrono::nanoseconds>(
348353
timeout);
349354
if (timeout_ns > std::chrono::nanoseconds::zero()) {
350355
end_time += timeout_ns;
351356
}
352357
std::chrono::nanoseconds timeout_left = timeout_ns;
353358

354-
if (spinning.exchange(true)) {
355-
throw std::runtime_error("spin_until_future_complete() called while already spinning");
356-
}
357359
RCPPUTILS_SCOPE_EXIT(this->spinning.store(false); );
358360
while (rclcpp::ok(this->context_) && spinning.load()) {
359361
// Do one item of work.
@@ -381,6 +383,47 @@ class Executor
381383
return FutureReturnCode::INTERRUPTED;
382384
}
383385

386+
/// Spin (blocking) until it times out, or rclcpp is interrupted.
387+
/**
388+
* \param[in] timeout Timeout parameter, which gets passed to Executor::spin_node_once.
389+
* \throws std::runtime_error if spinning is already taking place.
390+
*/
391+
template<typename TimeRepT = int64_t, typename TimeT = std::milli>
392+
void spin_until_timeout(
393+
std::chrono::duration<TimeRepT, TimeT> timeout)
394+
{
395+
auto end_time = std::chrono::steady_clock::now();
396+
397+
if (spinning.exchange(true)) {
398+
throw std::runtime_error("spin_until_timeout() called while already spinning");
399+
}
400+
401+
std::chrono::nanoseconds timeout_ns = std::chrono::duration_cast<std::chrono::nanoseconds>(
402+
timeout);
403+
if (timeout_ns <= std::chrono::nanoseconds::zero()) {
404+
// No work to be done, timeout is reached.
405+
return;
406+
}
407+
408+
end_time += timeout_ns;
409+
std::chrono::nanoseconds timeout_left = timeout_ns;
410+
411+
RCPPUTILS_SCOPE_EXIT(this->spinning.store(false); );
412+
while (rclcpp::ok(this->context_) && spinning.load()) {
413+
// Do one item of work.
414+
spin_once_impl(timeout_left);
415+
416+
// Otherwise check if we still have time to wait, return TIMEOUT if not.
417+
auto now = std::chrono::steady_clock::now();
418+
if (now >= end_time) {
419+
return;
420+
}
421+
422+
// Subtract the elapsed time from the original timeout.
423+
timeout_left = std::chrono::duration_cast<std::chrono::nanoseconds>(end_time - now);
424+
}
425+
}
426+
384427
/// Cancel any running spin* function, causing it to return.
385428
/**
386429
* This function can be called asynchonously from any thread.

rclcpp/include/rclcpp/executors.hpp

+52
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,39 @@ namespace executors
5454
using rclcpp::executors::MultiThreadedExecutor;
5555
using rclcpp::executors::SingleThreadedExecutor;
5656

57+
58+
/// Spin (blocking) until it times out, or rclcpp is interrupted.
59+
/**
60+
* \param[in] executor The executor which will spin the node.
61+
* \param[in] node_ptr The node to spin.
62+
* \param[in] timeout Timeout parameter, which gets passed to Executor::spin_node_once.
63+
* \throws std::runtime_error if spinning is already taking place.
64+
*/
65+
template<typename TimeRepT = int64_t, typename TimeT = std::milli>
66+
void
67+
spin_node_until_timeout(
68+
rclcpp::Executor & executor,
69+
rclcpp::node_interfaces::NodeBaseInterface::SharedPtr node_ptr,
70+
std::chrono::duration<TimeRepT, TimeT> timeout = std::chrono::duration<TimeRepT, TimeT>(-1))
71+
{
72+
executor.add_node(node_ptr);
73+
executor.spin_until_timeout(timeout);
74+
executor.remove_node(node_ptr);
75+
}
76+
77+
template<typename NodeT = rclcpp::Node, typename TimeRepT = int64_t, typename TimeT = std::milli>
78+
void
79+
spin_node_until_timeout(
80+
rclcpp::Executor & executor,
81+
std::shared_ptr<NodeT> node_ptr,
82+
std::chrono::duration<TimeRepT, TimeT> timeout = std::chrono::duration<TimeRepT, TimeT>(-1))
83+
{
84+
return rclcpp::executors::spin_node_until_timeout(
85+
executor,
86+
node_ptr->get_node_base_interface(),
87+
timeout);
88+
}
89+
5790
/// Spin (blocking) until the future is complete, it times out waiting, or rclcpp is interrupted.
5891
/**
5992
* \param[in] executor The executor which will spin the node.
@@ -100,6 +133,25 @@ spin_node_until_future_complete(
100133

101134
} // namespace executors
102135

136+
template<typename TimeRepT = int64_t, typename TimeT = std::milli>
137+
void
138+
spin_until_timeout(
139+
rclcpp::node_interfaces::NodeBaseInterface::SharedPtr node_ptr,
140+
std::chrono::duration<TimeRepT, TimeT> timeout = std::chrono::duration<TimeRepT, TimeT>(-1))
141+
{
142+
rclcpp::executors::SingleThreadedExecutor executor;
143+
return executors::spin_node_until_timeout(executor, node_ptr, timeout);
144+
}
145+
146+
template<typename NodeT = rclcpp::Node, typename TimeRepT = int64_t, typename TimeT = std::milli>
147+
rclcpp::FutureReturnCode
148+
spin_until_timeout(
149+
std::shared_ptr<NodeT> node_ptr,
150+
std::chrono::duration<TimeRepT, TimeT> timeout = std::chrono::duration<TimeRepT, TimeT>(-1))
151+
{
152+
return rclcpp::spin_until_timeout(node_ptr->get_node_base_interface(), timeout);
153+
}
154+
103155
template<typename FutureT, typename TimeRepT = int64_t, typename TimeT = std::milli>
104156
rclcpp::FutureReturnCode
105157
spin_until_future_complete(

rclcpp/test/rclcpp/executors/test_executors.cpp

+103-2
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,56 @@ TYPED_TEST(TestExecutors, spinWhileAlreadySpinning) {
221221
executor.remove_node(this->node, true);
222222
}
223223

224+
// Check if timeout is respected
225+
TYPED_TEST(TestExecutors, testSpinUntilTimeout) {
226+
using ExecutorType = TypeParam;
227+
ExecutorType executor;
228+
executor.add_node(this->node);
229+
230+
// block intil timeout is fulfilled.
231+
auto start = std::chrono::steady_clock::now();
232+
executor.spin_until_timeout(2s);
233+
executor.remove_node(this->node, true);
234+
// Check if timeout was fulfilled
235+
EXPECT_GT(2s, (std::chrono::steady_clock::now() - start));
236+
}
237+
238+
// Check if throws while spinner is already active
239+
TYPED_TEST(TestExecutors, testSpinUntilTimeoutAlreadySpinning) {
240+
using ExecutorType = TypeParam;
241+
ExecutorType executor;
242+
executor.add_node(this->node);
243+
244+
// Active spinner to check for collision.
245+
std::thread spinner([&]() {
246+
executor.spin_until_timeout(2s);
247+
});
248+
249+
// There already is an active spinner running
250+
// second call needs to throw.
251+
EXCEPT_THROW(executor.spin_until_timeout(1ms));
252+
executor.remove_node(this->node, true);
253+
executor.cancel();
254+
spinner.join();
255+
}
256+
257+
// Check if timeout is respected
258+
TYPED_TEST(TestExecutors, testSpinUntilTimeoutNegativeTimeout) {
259+
using ExecutorType = TypeParam;
260+
ExecutorType executor;
261+
executor.add_node(this->node);
262+
263+
// Block intil timeout is fulfilled.
264+
auto start = std::chrono::steady_clock::now();
265+
266+
// Spinner will exit immediately
267+
executor.spin_until_timeout(-1s);
268+
// Check if timeout existed without waiting
269+
// 100ms is an arbitrary picked duration
270+
EXPECT_LT(100ms, (std::chrono::steady_clock::now() - start));
271+
executor.remove_node(this->node, true);
272+
}
273+
224274
// Check executor exits immediately if future is complete.
225275
TYPED_TEST(TestExecutors, testSpinUntilFutureComplete) {
226276
using ExecutorType = TypeParam;
@@ -232,8 +282,7 @@ TYPED_TEST(TestExecutors, testSpinUntilFutureComplete) {
232282
std::future<bool> future = promise.get_future();
233283
promise.set_value(true);
234284

235-
// spin_until_future_complete is expected to exit immediately, but would block up until its
236-
// timeout if the future is not checked before spin_once_impl.
285+
// block spin until timeout is reached
237286
auto start = std::chrono::steady_clock::now();
238287
auto shared_future = future.share();
239288
auto ret = executor.spin_until_future_complete(shared_future, 1s);
@@ -482,6 +531,58 @@ TYPED_TEST(TestExecutors, spinSome) {
482531
spinner.join();
483532
}
484533

534+
// Check spin_node_until_timeout with node base pointer
535+
TYPED_TEST(TestExecutors, testSpinNodeUntilTimeoutNodeBasePtr) {
536+
using ExecutorType = TypeParam;
537+
ExecutorType executor;
538+
539+
auto start = std::chrono::steady_clock::now();
540+
EXCEPT_NO_THROW(
541+
rclcpp::executors::spin_node_until_timeout(
542+
executor, this->node->get_node_base_interface(), 500ms));
543+
544+
// Make sure that timeout was reached
545+
EXCEPT_GT(500ms, std::chrono::steady_clock::now() - start);
546+
}
547+
548+
// Check spin_node_until_timeout with node base pointer (instantiates its own executor)
549+
TEST(TestExecutors, testSpinNodeUntilTimeoutNodeBasePtr) {
550+
rclcpp::init(0, nullptr);
551+
552+
{
553+
auto node = std::make_shared<rclcpp::Node>("node");
554+
auto start = std::chrono::steady_clock::now();
555+
556+
EXCEPT_NO_THROW(
557+
rclcpp::spin_until_future_complete(
558+
node->get_node_base_interface(), shared_future, 500ms));
559+
560+
// Make sure that timeout was reached
561+
EXCEPT_GT(500ms, std::chrono::steady_clock::now() - start);
562+
}
563+
564+
rclcpp::shutdown();
565+
}
566+
567+
// Check spin_until_future_complete with node pointer (instantiates its own executor)
568+
TEST(TestExecutors, testSpinNodeUntilTimeoutNodePtr) {
569+
rclcpp::init(0, nullptr);
570+
571+
{
572+
auto node = std::make_shared<rclcpp::Node>("node");
573+
auto start = std::chrono::steady_clock::now();
574+
575+
EXCEPT_NO_THROW(
576+
rclcpp::spin_until_future_complete(
577+
node->get_node_base_interface(), shared_future, 500ms));
578+
579+
// Make sure that timeout was reached
580+
EXCEPT_GT(500ms, std::chrono::steady_clock::now() - start);
581+
}
582+
583+
rclcpp::shutdown();
584+
}
585+
485586
// Check spin_node_until_future_complete with node base pointer
486587
TYPED_TEST(TestExecutors, testSpinNodeUntilFutureCompleteNodeBasePtr) {
487588
using ExecutorType = TypeParam;

0 commit comments

Comments
 (0)