Skip to content

Commit 5929f64

Browse files
mwcondinogusbrigantino
authored andcommitted
[events executor] - Fix Behavior with Timer Cancel (ros2#2375)
* fix Signed-off-by: Matt Condino <[email protected]> * add timer cancel tests Signed-off-by: Matt Condino <[email protected]> * cleanup header include Signed-off-by: Matt Condino <[email protected]> * reverting change to timer_greater function Signed-off-by: Gus Brigantino <[email protected]> * use std::optional, and handle edgecase of 1 cancelled timer Signed-off-by: Matt Condino <[email protected]> * clean up run_timers func Signed-off-by: Gus Brigantino <[email protected]> * some fixes and added tests for cancel then reset of timers. Signed-off-by: Matt Condino <[email protected]> * refactor and clean up. remove cancelled timer tracking. Signed-off-by: Matt Condino <[email protected]> * remove unused method for size() Signed-off-by: Matt Condino <[email protected]> * linting Signed-off-by: Matt Condino <[email protected]> * relax timing constraints in tests Signed-off-by: Matt Condino <[email protected]> * further relax timing constraints to ensure windows tests are not flaky. Signed-off-by: Matt Condino <[email protected]> * use sim clock for tests, pub clock at .25 realtime rate. Signed-off-by: Matt Condino <[email protected]> --------- Signed-off-by: Matt Condino <[email protected]> Signed-off-by: Gus Brigantino <[email protected]> Co-authored-by: Gus Brigantino <[email protected]>
1 parent 1709df8 commit 5929f64

File tree

5 files changed

+468
-21
lines changed

5 files changed

+468
-21
lines changed

rclcpp/include/rclcpp/experimental/timers_manager.hpp

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,10 @@
2222
#include <functional>
2323
#include <memory>
2424
#include <mutex>
25+
#include <optional>
2526
#include <thread>
2627
#include <utility>
2728
#include <vector>
28-
2929
#include "rclcpp/context.hpp"
3030
#include "rclcpp/timer.hpp"
3131

@@ -174,13 +174,14 @@ class TimersManager
174174
* @brief Get the amount of time before the next timer triggers.
175175
* This function is thread safe.
176176
*
177-
* @return std::chrono::nanoseconds to wait,
177+
* @return std::optional<std::chrono::nanoseconds> to wait,
178178
* the returned value could be negative if the timer is already expired
179179
* or std::chrono::nanoseconds::max() if there are no timers stored in the object.
180+
* If the head timer was cancelled, then this will return a nullopt.
180181
* @throws std::runtime_error if the timers thread was already running.
181182
*/
182183
RCLCPP_PUBLIC
183-
std::chrono::nanoseconds get_head_timeout();
184+
std::optional<std::chrono::nanoseconds> get_head_timeout();
184185

185186
private:
186187
RCLCPP_DISABLE_COPY(TimersManager)
@@ -514,12 +515,13 @@ class TimersManager
514515
* @brief Get the amount of time before the next timer triggers.
515516
* This function is not thread safe, acquire a mutex before calling it.
516517
*
517-
* @return std::chrono::nanoseconds to wait,
518+
* @return std::optional<std::chrono::nanoseconds> to wait,
518519
* the returned value could be negative if the timer is already expired
519520
* or std::chrono::nanoseconds::max() if the heap is empty.
521+
* If the head timer was cancelled, then this will return a nullopt.
520522
* This function is not thread safe, acquire the timers_mutex_ before calling it.
521523
*/
522-
std::chrono::nanoseconds get_head_timeout_unsafe();
524+
std::optional<std::chrono::nanoseconds> get_head_timeout_unsafe();
523525

524526
/**
525527
* @brief Executes all the timers currently ready when the function is invoked

rclcpp/src/rclcpp/experimental/executors/events_executor/events_executor.cpp

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -208,11 +208,12 @@ EventsExecutor::spin_once_impl(std::chrono::nanoseconds timeout)
208208
timeout = std::chrono::nanoseconds::max();
209209
}
210210

211-
// Select the smallest between input timeout and timer timeout
211+
// Select the smallest between input timeout and timer timeout.
212+
// Cancelled timers are not considered.
212213
bool is_timer_timeout = false;
213214
auto next_timer_timeout = timers_manager_->get_head_timeout();
214-
if (next_timer_timeout < timeout) {
215-
timeout = next_timer_timeout;
215+
if (next_timer_timeout.has_value() && next_timer_timeout.value() < timeout) {
216+
timeout = next_timer_timeout.value();
216217
is_timer_timeout = true;
217218
}
218219

rclcpp/src/rclcpp/experimental/timers_manager.cpp

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ void TimersManager::stop()
102102
}
103103
}
104104

105-
std::chrono::nanoseconds TimersManager::get_head_timeout()
105+
std::optional<std::chrono::nanoseconds> TimersManager::get_head_timeout()
106106
{
107107
// Do not allow to interfere with the thread running
108108
if (running_) {
@@ -177,7 +177,7 @@ void TimersManager::execute_ready_timer(
177177
}
178178
}
179179

180-
std::chrono::nanoseconds TimersManager::get_head_timeout_unsafe()
180+
std::optional<std::chrono::nanoseconds> TimersManager::get_head_timeout_unsafe()
181181
{
182182
// If we don't have any weak pointer, then we just return maximum timeout
183183
if (weak_timers_heap_.empty()) {
@@ -199,7 +199,9 @@ std::chrono::nanoseconds TimersManager::get_head_timeout_unsafe()
199199
}
200200
head_timer = locked_heap.front();
201201
}
202-
202+
if (head_timer->is_canceled()) {
203+
return std::nullopt;
204+
}
203205
return head_timer->time_until_trigger();
204206
}
205207

@@ -255,17 +257,34 @@ void TimersManager::run_timers()
255257
// Lock mutex
256258
std::unique_lock<std::mutex> lock(timers_mutex_);
257259

258-
std::chrono::nanoseconds time_to_sleep = get_head_timeout_unsafe();
260+
std::optional<std::chrono::nanoseconds> time_to_sleep = get_head_timeout_unsafe();
261+
262+
// If head timer was cancelled, try to reheap and get a new head.
263+
// This avoids an edge condition where head timer is cancelled, but other
264+
// valid timers remain in the heap.
265+
if (!time_to_sleep.has_value()) {
266+
// Re-heap to (possibly) move cancelled timer from head of heap. If
267+
// entire heap is cancelled, this will still result in a nullopt.
268+
TimersHeap locked_heap = weak_timers_heap_.validate_and_lock();
269+
locked_heap.heapify();
270+
weak_timers_heap_.store(locked_heap);
271+
time_to_sleep = get_head_timeout_unsafe();
272+
}
259273

260-
// No need to wait if a timer is already available
261-
if (time_to_sleep > std::chrono::nanoseconds::zero()) {
262-
if (time_to_sleep != std::chrono::nanoseconds::max()) {
263-
// Wait until timeout or notification that timers have been updated
264-
timers_cv_.wait_for(lock, time_to_sleep, [this]() {return timers_updated_;});
265-
} else {
266-
// Wait until notification that timers have been updated
267-
timers_cv_.wait(lock, [this]() {return timers_updated_;});
268-
}
274+
// If no timers, or all timers cancelled, wait for an update.
275+
if (!time_to_sleep.has_value() || (time_to_sleep.value() == std::chrono::nanoseconds::max()) ) {
276+
// Wait until notification that timers have been updated
277+
timers_cv_.wait(lock, [this]() {return timers_updated_;});
278+
279+
// Re-heap in case ordering changed due to a cancelled timer
280+
// re-activating.
281+
TimersHeap locked_heap = weak_timers_heap_.validate_and_lock();
282+
locked_heap.heapify();
283+
weak_timers_heap_.store(locked_heap);
284+
} else if (time_to_sleep.value() != std::chrono::nanoseconds::zero()) {
285+
// If time_to_sleep is zero, we immediately execute. Otherwise, wait
286+
// until timeout or notification that timers have been updated
287+
timers_cv_.wait_for(lock, time_to_sleep.value(), [this]() {return timers_updated_;});
269288
}
270289

271290
// Reset timers updated flag

0 commit comments

Comments
 (0)