Skip to content

Re-key EventsCBGExecutor timers after a time jump - #3344

Draft
Yadunund wants to merge 1 commit into
rollingfrom
yadu/cbg-timer-queue-time-jump
Draft

Yadunund wants to merge 1 commit into
rollingfrom
yadu/cbg-timer-queue-time-jump

Conversation

@Yadunund

@Yadunund Yadunund commented Oct 14, 2026 •

Copy link
Copy Markdown
Member

Description

Fixes #3342

EventsCBGExecutor timers can stall after backward ROS time jumps or when use_sim_time is enabled at runtime because TimerQueue retains stale deadlines.

This PR uses the new schedule change callback from ros2/rcl#1352 to update timer deadlines. The callback only flags timers for rescheduling to avoid deadlocks.

test_executors_time_jump verifies both scenarios. It fails on Rolling and passes with both fixes.

Depends-On: ros2/rcl#1352

Is this user-facing behavior change?

Yes. Timers continue working after backward /clock jumps (e.g., ros2 bag play --loop --clock) and when use_sim_time is enabled at runtime.

Did you use Generative AI?

Yes. Claude Code (Claude Fable 5.1) assisted with investigation, implementation, and tests. Changes were reviewed and tested manually.

Additional Information

Reusing the on-reset callback causes a deadlock with TimeSource::set_clock().

@github-actions

github-actions Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

ABI Compliance Check

✅ Verdict: compatible

Library Verdict Summary
libcomponent_manager.so ✅ compatible No ABI changes detected.
librclcpp.so ✅ compatible No ABI changes detected.
librclcpp_action.so ✅ compatible No ABI changes detected.
librclcpp_lifecycle.so ✅ compatible No ABI changes detected.
✅ libcomponent_manager.so — full abidiff report

Compared:

  • Base: lib-base/libcomponent_manager.so
  • Head: lib-pr/libcomponent_manager.so @ 3637cae
(empty report — no differences printed by abidiff)
✅ librclcpp.so — full abidiff report

Compared:

  • Base: lib-base/librclcpp.so
  • Head: lib-pr/librclcpp.so @ 3637cae
Functions changes summary: 0 Removed, 0 Changed (112 filtered out), 0 Added (3 filtered out) functions
Variables changes summary: 0 Removed, 0 Changed, 0 Added variable


✅ librclcpp_action.so — full abidiff report

Compared:

  • Base: lib-base/librclcpp_action.so
  • Head: lib-pr/librclcpp_action.so @ 3637cae
(empty report — no differences printed by abidiff)
✅ librclcpp_lifecycle.so — full abidiff report

Compared:

  • Base: lib-base/librclcpp_lifecycle.so
  • Head: lib-pr/librclcpp_lifecycle.so @ 3637cae
(empty report — no differences printed by abidiff)

Updated for commit 3637cae · check run · suppressions: /home/runner/work/_temp/ros2-abi-suppressions.txt

@skyegalaxy

Copy link
Copy Markdown
Member

thanks for catching the issue and for the PR @Yadunund . @jmachowinski and I both looked into this during the client library working group today. My intuition is that this PR is perhaps putting in additional complexity in rclcpp's handling of timers for something that ought to be fixed in the rcl layer.

I think the preference to not invoke on_reset in ros2/rcl#995 has had the unintended consequence of pushing rcl timer logic further up. The ClockManager in rclpy is also doing something similar to this PR's new bookkeeping. So perhaps we could consider either re-using on_reset and notifying at both timer reschedule sites there, or adding something like rcl_timer_set_on_schedule_change_callback. I don't think we'd need to modify existing function signatures and can just add new symbols so we don't have to worry about ABI breaks.

@Yadunund

Yadunund commented Oct 16, 2026 •

Copy link
Copy Markdown
Member Author

@skyegalaxy thanks for the quick feedback! I agree a fix upstream in rcl would be much cleaner. I've opened ros2/rcl#1352 to add a rcl_timer_set_on_schedule_change_callback API.

I had my agent iterate with the on_reset approach and it apparently led to deadlocks in some cases so going with the callback approach and calling it at both sites.

Deadlock with `on_reset` Calling on_reset from both reschedule sites in `_rcl_timer_time_jump()` with the current `TimerQueue`. It fixes the stall, but it deadlocks the executor's timer thread against the time source. `TimeSource::set_clock()` holds the clock mutex while rcl runs the jump callbacks, so the on_reset handler now runs under it and takes the queue mutex and then the wait mutex in `wakeup_timer_thread()`, while the timer thread takes the wait mutex and then the clock mutex every time `ClockWaiterImpl::wait_until_ros_time()` installs or removes its jump handler. TSAN (`detect_deadlocks=1`) reports the inversion within seconds of backward jumps, and a stress with 64 ROS time timers and a 1 s backward `/clock` jump every 25 samples hangs within a minute when pinned to one CPU (`taskset -c 0`):

Also updated this PR to adopt the new callback.

@Yadunund
Yadunund force-pushed the yadu/cbg-timer-queue-time-jump branch from 7bbb95f to aabbea0 Compare October 16, 2026 21:30
rcl moves next_call_time of a ROS time timer when the clock jumps
back by more than one period and when ROS time is activated or
deactivated, without calling the on reset callback. TimerQueue keyed
its running timers by a copy of next_call_time taken when the timer
was added, fired or reset, so after such a jump the timer thread slept
until the stale deadline: a /clock jump from 1000 s back to 900 s
silenced a 100 ms timer for 100 s of simulated time, and a timer that
was created before use_sim_time was enabled never fired again.

Register rcl's schedule change callback on every ROS time timer. It
runs inside the clock jump callback with the clock mutex held, which
the timer thread takes while holding its wait mutex, so it only flags
the timer; the timer thread re-keys flagged timers before it computes
its next wakeup, and the flag is part of its wait predicate.

Signed-off-by: Yadunund Vijay <yadunund@gmail.com>
@Yadunund
Yadunund force-pushed the yadu/cbg-timer-queue-time-jump branch from aabbea0 to 3637cae Compare October 16, 2026 22:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

EventsCBGExecutor timers on ROS time stall after a backward /clock jump

2 participants