Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
4d506c9
fix(pubsub): stop running subscriber callbacks on the publishing thread
YuanYuYuan Jul 27, 2026
ccac95f
fix(pubsub): guard the raw publish path and stop mislabelling drops
YuanYuYuan Jul 28, 2026
9e6b2ed
revert(tests): drop the FFI raw-publish test and its feature
YuanYuYuan Jul 28, 2026
709e465
fix(py,pubsub): unblock teardown -- GIL deadlock and unbounded drop
YuanYuYuan Jul 29, 2026
3cd795f
test(ros): fail the interop job when it runs no tests
YuanYuYuan Jul 29, 2026
193076c
style: wrap two long assert! calls in the queue tests
YuanYuYuan Jul 29, 2026
486d68f
test(pubsub): pin the async publish path's local-publish guard
YuanYuYuan Jul 29, 2026
211cae3
refactor: split the CI gate and the zero-copy callback out
YuanYuYuan Jul 29, 2026
5151a3a
fix(pubsub): stop giving queue-mode TransientLocal subs a dispatcher
YuanYuYuan Jul 29, 2026
e278884
fix(pubsub): box both dispatcher-callback arms to a common type
YuanYuYuan Jul 29, 2026
2aac599
fix(ffi): update the raw subscriber for Option<CallbackDispatcher>
YuanYuYuan Jul 29, 2026
c9a1940
fix(ci): stop reverting #271's interop-output fix
YuanYuYuan Aug 5, 2026
1c8e360
fix(pubsub): bound the advanced dispatch queue at the history depth
YuanYuYuan Aug 5, 2026
5f4a0c1
Revert "fix(pubsub): bound the advanced dispatch queue at the history…
YuanYuYuan Aug 6, 2026
c6ef483
Reapply "fix(pubsub): bound the advanced dispatch queue at the histor…
YuanYuYuan Aug 6, 2026
3aff1ae
test(pubsub): assert delivery ordering, not completeness, on the boun…
YuanYuYuan Aug 6, 2026
d2fbb84
fix(node): bound the FFI advanced dispatch queue too
YuanYuYuan Aug 6, 2026
e7e28bb
docs(pubsub): correct why the FFI arm's wrong constant was missed
YuanYuYuan Aug 6, 2026
d6d0f2d
docs(pubsub): apply simplified technical english to the dispatcher docs
YuanYuYuan Aug 12, 2026
6865ced
docs: apply ste sentence rules to added comments
YuanYuYuan Aug 14, 2026
f724a0a
fix(pubsub): disclose the teardown block and pin the gating decision
YuanYuYuan Aug 14, 2026
5aa7e38
docs(pubsub): state the drop-order obligation on Plain too
YuanYuYuan Aug 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 24 additions & 2 deletions crates/hiroz-py/src/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,20 @@ pub struct PyZNode {
next_sub_id: u64,
}

impl Drop for PyZNode {
fn drop(&mut self) {
// Same hazard as `destroy_subscriber`, reached a different way: `del
// node` and interpreter shutdown run `tp_dealloc` with the GIL held,
// and dropping these joins delivery threads whose callbacks need the
// GIL. Take the subscribers out and drop them with the GIL released.
if self.owned_subs.is_empty() {
return;
}
let subs = std::mem::take(&mut self.owned_subs);
Python::with_gil(move |py| py.allow_threads(move || drop(subs)));
}
}

#[allow(unsafe_op_in_unsafe_fn)]
#[pymethods]
impl PyZNode {
Expand Down Expand Up @@ -410,14 +424,22 @@ impl PyZNode {
///
/// Matches rclpy's `Node.destroy_subscription()`. Has no effect on queue-based
/// subscribers (those are owned by the caller and dropped when they go out of scope).
fn destroy_subscriber(&mut self, sub: &PyZSubscriber) -> PyResult<()> {
fn destroy_subscriber(&mut self, py: Python<'_>, sub: &PyZSubscriber) -> PyResult<()> {
let Some(id) = sub.owned_id else {
return Err(pyo3::exceptions::PyValueError::new_err(
"destroy_subscriber only applies to callback-based subscribers",
));
};
if let Some(pos) = self.owned_subs.iter().position(|(sid, _)| *sid == id) {
self.owned_subs.swap_remove(pos);
let owned = self.owned_subs.swap_remove(pos);
// Drop with the GIL released. Dropping a `ZSub` joins its delivery
// thread, and that thread's callback body is `Python::with_gil`. A
// `#[pymethods]` fn runs with the GIL held, so joining from here
// while the thread is mid-callback is a deadlock: we wait for it,
// it waits for the GIL we are holding. The interpreter freezes with
// no exception and no traceback -- the same failure this type
// exists to remove, relocated to teardown.
py.allow_threads(move || drop(owned));
}
Ok(())
}
Expand Down
22 changes: 16 additions & 6 deletions crates/hiroz-py/src/pubsub.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,20 +24,30 @@ impl PyZPublisher {
/// Publish a message
///
/// Serializes the Python message (msgspec.Struct) to ZBuf and publishes (zero-copy path)
unsafe fn publish(&self, _py: Python, data: &Bound<'_, PyAny>) -> PyResult<()> {
// Serialize Python message directly to ZBuf (zero-copy)
unsafe fn publish(&self, py: Python, data: &Bound<'_, PyAny>) -> PyResult<()> {
// Serialize Python message directly to ZBuf (zero-copy). This touches
// Python objects, so it must run with the GIL held.
let zbuf = hiroz_msgs::serialize_to_zbuf(&self.type_name, data)?;

// Publish the ZBuf directly
self.inner.publish(zbuf.into()).map_err(|e| e.into_pyerr())
// Release the GIL for the publish itself. Zenoh delivers samples to
// local subscribers synchronously on the publishing thread, so a publish
// issued from inside a subscriber callback can block here; holding the
// GIL across it would freeze the whole interpreter — no exception, no
// traceback — instead of blocking just this thread.
py.allow_threads(|| self.inner.publish(zbuf.into()))
.map_err(|e| e.into_pyerr())
}

/// Publish pre-serialized CDR bytes directly
///
/// Use this for zero-copy forwarding of received messages (e.g., in a pong responder).
/// The bytes should be in CDR format (as returned by recv_serialized/try_recv_serialized).
fn publish_raw(&self, data: &[u8]) -> PyResult<()> {
self.inner.publish(data.into()).map_err(|e| e.into_pyerr())
fn publish_raw(&self, py: Python, data: &[u8]) -> PyResult<()> {
// Copy out of the Python buffer before dropping the GIL, then publish
// without it — see `publish` for why.
let payload: zenoh::bytes::ZBytes = data.into();
py.allow_threads(|| self.inner.publish(payload))
.map_err(|e| e.into_pyerr())
}

/// Get the topic name (for debugging)
Expand Down
Loading
Loading