Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
549a530
fix(pubsub): stop running subscriber callbacks on the publishing thread
YuanYuYuan Jul 27, 2026
f1185b6
fix(pubsub): guard the raw publish path and stop mislabelling drops
YuanYuYuan Jul 28, 2026
b4be6fc
revert(tests): drop the FFI raw-publish test and its feature
YuanYuYuan Jul 28, 2026
75742bc
fix(py,pubsub): unblock teardown -- GIL deadlock and unbounded drop
YuanYuYuan Jul 29, 2026
830c1cb
test(ros): fail the interop job when it runs no tests
YuanYuYuan Jul 29, 2026
75959f6
style: wrap two long assert! calls in the queue tests
YuanYuYuan Jul 29, 2026
e40fdf9
test(pubsub): pin the async publish path's local-publish guard
YuanYuYuan Jul 29, 2026
6121b58
refactor: split the CI gate and the zero-copy callback out
YuanYuYuan Jul 29, 2026
800c069
fix(pubsub): stop giving queue-mode TransientLocal subs a dispatcher
YuanYuYuan Jul 29, 2026
6fa36b9
fix(pubsub): box both dispatcher-callback arms to a common type
YuanYuYuan Jul 29, 2026
08e994d
fix(ffi): update the raw subscriber for Option<CallbackDispatcher>
YuanYuYuan Jul 29, 2026
49e0baf
fix(ci): stop reverting #271's interop-output fix
YuanYuYuan Aug 5, 2026
3301509
fix(pubsub): bound the advanced dispatch queue at the history depth
YuanYuYuan Aug 5, 2026
51f1dca
Revert "fix(pubsub): bound the advanced dispatch queue at the history…
YuanYuYuan Aug 6, 2026
9b04c37
Reapply "fix(pubsub): bound the advanced dispatch queue at the histor…
YuanYuYuan Aug 6, 2026
bd2cf33
test(pubsub): assert delivery ordering, not completeness, on the boun…
YuanYuYuan Aug 6, 2026
b2a305e
fix(node): bound the FFI advanced dispatch queue too
YuanYuYuan Aug 6, 2026
b326f33
docs(pubsub): correct why the FFI arm's wrong constant was missed
YuanYuYuan Aug 6, 2026
cb31301
docs(pubsub): apply simplified technical english to the dispatcher docs
YuanYuYuan Aug 12, 2026
a6ab408
docs: apply ste sentence rules to added comments
YuanYuYuan Aug 14, 2026
489c994
fix(pubsub): disclose the teardown block and pin the gating decision
YuanYuYuan Aug 14, 2026
7d9d415
docs(pubsub): state the drop-order obligation on Plain too
YuanYuYuan Aug 14, 2026
266b0d9
fix(pubsub): stop making drop(subscriber) a barrier on the in-flight …
YuanYuYuan Aug 17, 2026
58f59e6
fix(pubsub): guard the plain path's inline callback too
YuanYuYuan Aug 17, 2026
e343d51
style(node): rustfmt the ported import block
YuanYuYuan Aug 17, 2026
f89f309
test(pubsub): pin the inline panic guard, both directions
YuanYuYuan Aug 18, 2026
c631096
test(ci): count detector outcomes from the failures block
YuanYuYuan Aug 18, 2026
7eeab06
chore: drop the CI verification artifacts, rustfmt the new test
YuanYuYuan Aug 18, 2026
115889f
test(pubsub): migrate the teardown test to close(deadline)
YuanYuYuan Aug 18, 2026
14d4a8e
style(tests): rustfmt the panic guard test
YuanYuYuan Aug 18, 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
34 changes: 32 additions & 2 deletions crates/hiroz-py/src/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,24 @@ 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.
//
// Dropping a `ZSub` no longer joins its delivery thread (it detaches --
// see `CallbackDispatcher::drop`), so this wrapper is belt-and-braces
// rather than load-bearing. It is kept because it costs nothing and it
// documents the hazard for anyone who adds a barrier here later.
// 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 +428,26 @@ 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` no longer joins its delivery thread -- it
// detaches, see `CallbackDispatcher::drop` -- so this wrapper is
// belt-and-braces rather than load-bearing. It is kept because it
// costs nothing and it documents the hazard it used to prevent:
// that thread's callback body is `Python::with_gil`, a
// `#[pymethods]` fn runs with the GIL held, and any barrier added
// here would wait for a thread that is waiting for the GIL we hold.
// The interpreter would freeze with no exception and no traceback.
// Anyone reaching for `ZSub::close` here must keep this wrapper.
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