Skip to content

Commit ae4f0f8

Browse files
authored
fix(event,graph): run event callbacks outside the locks they live under (#260)
1 parent af00f1e commit ae4f0f8

9 files changed

Lines changed: 991 additions & 179 deletions

File tree

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
//! Re-entrancy audit for endpoint event-status updates.
2+
//!
3+
//! `EventsManager` is shared as `Arc<Mutex<..>>`, so `&mut self` proves the
4+
//! caller holds that mutex — and `update_event_status` fired the callback from
5+
//! there. The callback is user code the rmw layer hands to an rclcpp executor,
6+
//! and the first thing it usually does is ask the handle that fired for its
7+
//! status (`rmw_take_event` → `RmEventHandle::take_event`), locking the same
8+
//! mutex on the same thread. Non-reentrant, no race needed.
9+
//!
10+
//! Fix: record under the lock, drop the guard, invoke.
11+
//! `record_event_status_with_policy` returns the callback rather than calling
12+
//! it, and `update_shared_event_status[_with_policy]` is what holders use.
13+
//!
14+
//! Each scenario runs on its own thread behind a deadline, so a deadlock fails
15+
//! the test instead of wedging the suite.
16+
//!
17+
//! # What these detect, and what they do not
18+
//!
19+
//! Both call `update_shared_event_status`, which this change *introduces* — so
20+
//! a wholesale revert does not turn them red, it stops this file compiling.
21+
//! They are not evidence the old shape deadlocked; `reentrant_graph_event.rs`
22+
//! carries that.
23+
//!
24+
//! They are still detectors, for the property rather than the history:
25+
//! reinstate the callout inside `update_shared_event_status_with_policy` and
26+
//! both fail on their deadline. That is the regression worth guarding, since
27+
//! the old entry point is gone.
28+
29+
use std::{
30+
sync::{
31+
Arc, Mutex,
32+
atomic::{AtomicI32, Ordering},
33+
mpsc,
34+
},
35+
thread,
36+
time::Duration,
37+
};
38+
39+
use hiroz::{
40+
GidArray,
41+
event::{
42+
EventsManager, RmEventHandle, ZenohEventType, update_shared_event_status,
43+
update_shared_event_status_with_policy,
44+
},
45+
};
46+
47+
/// Budget for one scenario. Generous relative to the work done — anything
48+
/// slower than this is a hang, not slowness.
49+
const SCENARIO_TIMEOUT: Duration = Duration::from_secs(30);
50+
51+
/// Run `scenario` on its own thread; fail (rather than hang) past the deadline.
52+
///
53+
/// On timeout the worker is deliberately left running: it is blocked on a lock
54+
/// that will never be released, and there is no sound way to unwind it.
55+
fn with_deadline(name: &'static str, scenario: impl FnOnce() + Send + 'static) {
56+
let (tx, rx) = mpsc::channel();
57+
thread::spawn(move || {
58+
scenario();
59+
let _ = tx.send(());
60+
});
61+
match rx.recv_timeout(SCENARIO_TIMEOUT) {
62+
Ok(()) => {}
63+
// The worker panicked and dropped the sender. That is an assertion
64+
// failure inside the scenario, NOT a deadlock — reporting it as one
65+
// would turn every ordinary test failure into a false deadlock report.
66+
Err(mpsc::RecvTimeoutError::Disconnected) => {
67+
panic!("{name}: scenario panicked — see the worker thread's panic above")
68+
}
69+
Err(mpsc::RecvTimeoutError::Timeout) => {
70+
panic!("{name}: scenario did not finish within {SCENARIO_TIMEOUT:?} — deadlock")
71+
}
72+
}
73+
}
74+
75+
fn gid(n: u8) -> GidArray {
76+
let mut g = [0u8; 16];
77+
g[0] = n;
78+
g
79+
}
80+
81+
/// The canonical rmw shape: the matched-event callback immediately takes the
82+
/// status that triggered it.
83+
///
84+
/// `take_event` locks the same `Arc<Mutex<EventsManager>>` the update path
85+
/// holds, so with the callback invoked under that guard this never returns.
86+
#[test]
87+
fn event_callback_taking_its_own_status_does_not_deadlock() {
88+
with_deadline("event_callback_take_event", || {
89+
let mgr = Arc::new(Mutex::new(EventsManager::new(gid(1))));
90+
let handle = Arc::new(RmEventHandle::new(
91+
mgr.clone(),
92+
ZenohEventType::SubscriptionMatched,
93+
));
94+
95+
let observed = Arc::new(AtomicI32::new(-1));
96+
{
97+
let handle_in_cb = handle.clone();
98+
let observed = observed.clone();
99+
handle.set_callback(move |_change| {
100+
let status = handle_in_cb.take_event();
101+
observed.store(status.total_count, Ordering::SeqCst);
102+
});
103+
}
104+
105+
update_shared_event_status(&mgr, ZenohEventType::SubscriptionMatched, 1);
106+
107+
assert_eq!(
108+
observed.load(Ordering::SeqCst),
109+
1,
110+
"the callback did not observe the status change that triggered it"
111+
);
112+
// The callback consumed the change counters via `take_event`.
113+
assert!(
114+
!handle.is_ready(),
115+
"take_event inside the callback should have cleared the changed flag"
116+
);
117+
});
118+
}
119+
120+
/// A QoS-incompatibility callback that re-arms itself.
121+
///
122+
/// `set_callback` locks the manager to install, so a callback that replaces
123+
/// itself re-enters the outer mutex exactly like `take_event` does. This also
124+
/// covers the `_with_policy` entry point, which carries the encoded policy kind.
125+
#[test]
126+
fn event_callback_reinstalling_itself_does_not_deadlock() {
127+
with_deadline("event_callback_reinstall", || {
128+
let mgr = Arc::new(Mutex::new(EventsManager::new(gid(2))));
129+
let handle = Arc::new(RmEventHandle::new(
130+
mgr.clone(),
131+
ZenohEventType::RequestedQosIncompatible,
132+
));
133+
134+
let fired = Arc::new(AtomicI32::new(0));
135+
{
136+
let handle_in_cb = handle.clone();
137+
let fired = fired.clone();
138+
handle.set_callback(move |_change| {
139+
fired.fetch_add(1, Ordering::SeqCst);
140+
// Re-arm with a no-op. Installing takes the manager lock.
141+
handle_in_cb.set_callback(|_| {});
142+
});
143+
}
144+
145+
update_shared_event_status_with_policy(
146+
&mgr,
147+
ZenohEventType::RequestedQosIncompatible,
148+
1,
149+
42,
150+
);
151+
152+
assert_eq!(
153+
fired.load(Ordering::SeqCst),
154+
1,
155+
"the re-arming callback did not run"
156+
);
157+
let status = handle.take_event();
158+
assert_eq!(status.total_count, 1);
159+
assert_eq!(status.last_policy_kind, 42);
160+
});
161+
}

0 commit comments

Comments
 (0)