Repository navigation
Fix deadlock in TimeSource::destroy_clock_sub (#2962) - #3412
SIDDARTHAREDDY8 wants to merge 1 commit into
Conversation
|
Thanks for revisiting #2962 and incorporating the concurrent-create concern from #3167, @SIDDARTHAREDDY8. Moving the subscription under the lock and capturing the executor/group by value look like useful improvements. One interleaving may still need guarding: |
destroy_clock_sub() held clock_sub_lock_ while joining clock_executor_thread_. If the executor thread's shutdown path contends on the same lock, the join deadlocks (see the gdb trace in ros2#2962: the main thread waits in join() holding clock_sub_lock_). Move the thread, executor, callback group and subscription into locals under the lock, release the lock, then cancel/join outside the critical section. The subscription is moved (not reset) under the lock, so a concurrent create_clock_sub() sees no subscription and builds a fresh one instead of returning early on a stale subscription that this function would then reset, leaving sim time enabled with no subscription. The old subscription stays alive in a local until after the join, so the executor thread can finish any in-flight callback without touching a destroyed subscription. The teardown repeats if a concurrent create installed a new subscription/thread while joining, so destroy never returns with a clock executor thread still spinning behind it. The clock executor thread now captures its executor and callback group by value instead of reading the clock_executor_ / clock_callback_group_ members: those members are moved out by destroy_clock_sub(), and a thread starting after the move would otherwise dereference a null executor. Adds test_time_source_deadlock.cpp: rapid construct/destroy with use_sim_time, concurrent construct/destroy, toggling use_sim_time while spinning, no lingering thread after destroy, and rapid construct/destroy under /clock traffic. Follow-up: close the detach/create race flagged in review of ros2#3412. detachNode() calls destroy_clock_sub(), whose final empty-state check can return; a concurrent post_set_parameters(true) could then enter create_clock_sub() before detachNode() acquires node_base_lock_ and clears the interfaces, leaving a newly started clock thread using the interfaces during detach. A detaching_ flag, set by detachNode() under clock_sub_lock_ before any teardown and cleared by attachNode(), makes create_clock_sub() refuse to start a new clock executor thread while a detach is in progress. Adds detach_races_sim_time_enable: detach racing a use_sim_time toggler, then re-attach and a clean detach. Fixes ros2#2962 Signed-off-by: Siddartha Reddy Chinthala <siddarthareddychinthala@gmail.com> Signed-off-by: SIDDARTHA REDDY <75976672+SIDDARTHAREDDY8@users.noreply.github.com>
bc4b123 to
b23fc6f
Compare
|
Addressed both review points in b23fc6f (the branch is now a single commit on the same rolling base, same 3 files):
Verified locally with a rolling build: |
sylvesterkaczmarek
left a comment
There was a problem hiding this comment.
Thanks for the follow-up, @SIDDARTHAREDDY8. Rechecked b23fc6f: detaching_ is set under clock_sub_lock_ before teardown and prevents create_clock_sub() from restarting the executor in the window I flagged. The new detach/enable stress case covers that path, and DCO plus the hosted build now pass. That resolves both issues I raised. I have not run a separate ROS build locally; no remaining blocker from this review.
Description
Fixes #2962
TimeSource::NodeState::destroy_clock_sub()heldclock_sub_lock_while callingclock_executor_thread_.join(). If the executor thread's shutdown path contends on the same lock, the join deadlocks: the main thread waits for the executor thread to finish while the executor thread waits for the main thread to release the lock (see the gdb trace in #2962, where the main thread is stuck injoin()holdingclock_sub_lock_).The fix: move the thread, executor, callback group and subscription into locals under the lock, release the lock, then cancel and join outside the critical section.
Two details beyond the approach sketched in the earlier attempts (#3110, #3167):
clock_subscription_alive until after the join and reset it under a second lock acquisition. As @sylvesterkaczmarek noted there, a concurrentcreate_clock_sub()can then see the still-live subscription and return early, and this path resets it after the join, leaving sim time enabled with no subscription. Moving the subscription to a local under the first lock closes that window: a concurrent create sees no subscription and builds a fresh one. The old subscription is kept alive in a local until after the join, so the executor thread can finish any in-flight callback without touching a destroyed subscription.create_clock_sub()installed a new subscription/thread while the old thread was being joined. This is only possible when a parameter callback racesdetachNode()(post-set-parameter callbacks are serialized by the node parameters mutex), butdestroy_clock_sub()must never return with a clock executor thread still spinning behind it:detachNode()resets the node interfaces right after, and the executor thread's lambda capturesthis.The clock executor thread now also captures its executor and callback group by value instead of reading the
clock_executor_/clock_callback_group_members: those members are moved out bydestroy_clock_sub(), and a thread starting after the move would otherwise dereference a null executor. (I hit this as a segfault in local testing of the first version of this patch; the by-value capture fixed it.)Adds
test_time_source_deadlock.cpp: rapid construct/destroy withuse_sim_time=true, concurrent construct/destroy across threads, togglinguse_sim_timewhile spinning, no lingering thread after destroy, and rapid construct/destroy under live/clocktraffic. If the deadlock is present these hang and trip the test timeout.Note on the earlier attempts: #3110 was closed over an AI-disclosure violation (disclosure is given below); #3167 was closed by its author on the belief that #3174 fixed this issue, but #3174 fixed #3172 (honoring
cancel()issued beforespin_until_future_complete()), not #2962, which is still open.Is this user-facing behavior change?
No, except that node teardown with
use_sim_timeno longer sporadically hangs.Did you use Generative AI?
Yes, Muse Spark (Meta) was used as an assistive coding tool for drafting this change and the regression tests. I reviewed the code, ran the tests below, and take responsibility for the contribution's validity.
Test plan
Built
rclcppfrom source (rolling HEAD917c1240) and ran, withRMW_IMPLEMENTATION=rmw_zenoh_cpp:test_time_source_deadlock: 5/5 pass, 10 consecutive runs, plus 36 parallel runs under load -- no hangstest_time_source: 18/18 passtest_node: 50/50 passament_uncrustifyandament_cpplintclean on the changed filesHonest note on reproduction: I could not reproduce the hang on this machine (100+ stress runs on the unfixed code, including parallel runs under load, all passed). That matches the issue thread itself: the reporter reproduced it on one laptop but not on others, and a maintainer could not reproduce it in 100 runs on a 28-core machine. The race is timing/hardware dependent; the stress tests exercise the exact teardown window and guard against regressions.