Skip to content

Commit 99f0fc9

Browse files
[events executor] - Fix Behavior with Timer Cancel (#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 265f5ec commit 99f0fc9

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

@@ -172,13 +172,14 @@ class TimersManager
172172
* @brief Get the amount of time before the next timer triggers.
173173
* This function is thread safe.
174174
*
175-
* @return std::chrono::nanoseconds to wait,
175+
* @return std::optional<std::chrono::nanoseconds> to wait,
176176
* the returned value could be negative if the timer is already expired
177177
* or std::chrono::nanoseconds::max() if there are no timers stored in the object.
178+
* If the head timer was cancelled, then this will return a nullopt.
178179
* @throws std::runtime_error if the timers thread was already running.
179180
*/
180181
RCLCPP_PUBLIC
181-
std::chrono::nanoseconds get_head_timeout();
182+
std::optional<std::chrono::nanoseconds> get_head_timeout();
182183

183184
private:
184185
RCLCPP_DISABLE_COPY(TimersManager)
@@ -512,12 +513,13 @@ class TimersManager
512513
* @brief Get the amount of time before the next timer triggers.
513514
* This function is not thread safe, acquire a mutex before calling it.
514515
*
515-
* @return std::chrono::nanoseconds to wait,
516+
* @return std::optional<std::chrono::nanoseconds> to wait,
516517
* the returned value could be negative if the timer is already expired
517518
* or std::chrono::nanoseconds::max() if the heap is empty.
519+
* If the head timer was cancelled, then this will return a nullopt.
518520
* This function is not thread safe, acquire the timers_mutex_ before calling it.
519521
*/
520-
std::chrono::nanoseconds get_head_timeout_unsafe();
522+
std::optional<std::chrono::nanoseconds> get_head_timeout_unsafe();
521523

522524
/**
523525
* @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
@@ -206,11 +206,12 @@ EventsExecutor::spin_once_impl(std::chrono::nanoseconds timeout)
206206
timeout = std::chrono::nanoseconds::max();
207207
}
208208

209-
// Select the smallest between input timeout and timer timeout
209+
// Select the smallest between input timeout and timer timeout.
210+
// Cancelled timers are not considered.
210211
bool is_timer_timeout = false;
211212
auto next_timer_timeout = timers_manager_->get_head_timeout();
212-
if (next_timer_timeout < timeout) {
213-
timeout = next_timer_timeout;
213+
if (next_timer_timeout.has_value() && next_timer_timeout.value() < timeout) {
214+
timeout = next_timer_timeout.value();
214215
is_timer_timeout = true;
215216
}
216217

rclcpp/src/rclcpp/experimental/timers_manager.cpp

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

103-
std::chrono::nanoseconds TimersManager::get_head_timeout()
103+
std::optional<std::chrono::nanoseconds> TimersManager::get_head_timeout()
104104
{
105105
// Do not allow to interfere with the thread running
106106
if (running_) {
@@ -169,7 +169,7 @@ void TimersManager::execute_ready_timer(const rclcpp::TimerBase * timer_id)
169169
}
170170
}
171171

172-
std::chrono::nanoseconds TimersManager::get_head_timeout_unsafe()
172+
std::optional<std::chrono::nanoseconds> TimersManager::get_head_timeout_unsafe()
173173
{
174174
// If we don't have any weak pointer, then we just return maximum timeout
175175
if (weak_timers_heap_.empty()) {
@@ -191,7 +191,9 @@ std::chrono::nanoseconds TimersManager::get_head_timeout_unsafe()
191191
}
192192
head_timer = locked_heap.front();
193193
}
194-
194+
if (head_timer->is_canceled()) {
195+
return std::nullopt;
196+
}
195197
return head_timer->time_until_trigger();
196198
}
197199

@@ -242,17 +244,34 @@ void TimersManager::run_timers()
242244
// Lock mutex
243245
std::unique_lock<std::mutex> lock(timers_mutex_);
244246

245-
std::chrono::nanoseconds time_to_sleep = get_head_timeout_unsafe();
247+
std::optional<std::chrono::nanoseconds> time_to_sleep = get_head_timeout_unsafe();
248+
249+
// If head timer was cancelled, try to reheap and get a new head.
250+
// This avoids an edge condition where head timer is cancelled, but other
251+
// valid timers remain in the heap.
252+
if (!time_to_sleep.has_value()) {
253+
// Re-heap to (possibly) move cancelled timer from head of heap. If
254+
// entire heap is cancelled, this will still result in a nullopt.
255+
TimersHeap locked_heap = weak_timers_heap_.validate_and_lock();
256+
locked_heap.heapify();
257+
weak_timers_heap_.store(locked_heap);
258+
time_to_sleep = get_head_timeout_unsafe();
259+
}
246260

247-
// No need to wait if a timer is already available
248-
if (time_to_sleep > std::chrono::nanoseconds::zero()) {
249-
if (time_to_sleep != std::chrono::nanoseconds::max()) {
250-
// Wait until timeout or notification that timers have been updated
251-
timers_cv_.wait_for(lock, time_to_sleep, [this]() {return timers_updated_;});
252-
} else {
253-
// Wait until notification that timers have been updated
254-
timers_cv_.wait(lock, [this]() {return timers_updated_;});
255-
}
261+
// If no timers, or all timers cancelled, wait for an update.
262+
if (!time_to_sleep.has_value() || (time_to_sleep.value() == std::chrono::nanoseconds::max()) ) {
263+
// Wait until notification that timers have been updated
264+
timers_cv_.wait(lock, [this]() {return timers_updated_;});
265+
266+
// Re-heap in case ordering changed due to a cancelled timer
267+
// re-activating.
268+
TimersHeap locked_heap = weak_timers_heap_.validate_and_lock();
269+
locked_heap.heapify();
270+
weak_timers_heap_.store(locked_heap);
271+
} else if (time_to_sleep.value() != std::chrono::nanoseconds::zero()) {
272+
// If time_to_sleep is zero, we immediately execute. Otherwise, wait
273+
// until timeout or notification that timers have been updated
274+
timers_cv_.wait_for(lock, time_to_sleep.value(), [this]() {return timers_updated_;});
256275
}
257276

258277
// Reset timers updated flag

0 commit comments

Comments
 (0)