Skip to content

feat(hiroz)!: let a raw sub join the cpu group - #385

Merged
YuanYuYuan merged 211 commits into
mainfrom
feat/raw-subscriber-buffer-group-3
Oct 23, 2026
Merged

YuanYuYuan merged 211 commits into
mainfrom
feat/raw-subscriber-buffer-group-3

Conversation

@YuanYuYuan

@YuanYuYuan YuanYuYuan commented Oct 16, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

A buffer-aware rmw_zenoh_cpp publisher stops writing the plain topic key for a message with an unbounded uint8[] or char[] field. It writes <topic key>/_buf_cpu for subscribers whose liveliness token ends with backends:cpu:. See ros2/rmw_zenoh#930. This text calls <topic key> the base key and <topic key>/_buf_cpu the group key.

A raw subscriber declared no token and read only the base key. It received nothing from such a publisher, with no error and no log. This is the report in #369.

Part of #372.

What changes

  • A raw subscriber joins the CPU group by default. It has no message schema to inspect, so Auto joins it, as it joins a typed subscriber without a schema.
  • create_raw_subscriber and create_raw_subscriber_with_qos use Auto. The C and Go bindings call them, so they join too.
  • New method ZNode::create_raw_subscriber_with_group, only with the ffi feature. It takes Auto, Cpu or Off. Off stays out and declares no token. The Go bindings have no entry point for it yet.
  • A joined raw subscriber declares two exact subscriptions, on the base key and on the group key, and then a token that ends with backends:cpu:. One callback serves both keys.
  • It stays out with transient-local QoS or a custom key format, whatever the group. The feedback subscriber of a raw action client reads nothing and stays out.
  • RawSubscriber has no public field now. RawSubscriber::undeclare consumes the wrapper and stops the token and both subscriptions. Dropping the wrapper stops the same three handles in the same order.
  • The interop guide states the new behaviour, the migration, and the dependency on ros2/rmw_zenoh#1078 for subscribers with no backends: field.
  • CI runs the raw test suite as its own cargo nextest run --no-tests=fail step. Without the raw-sub-tests feature the suite compiles to nothing, and the step fails instead of passing with zero tests.

Before and after

case today with this PR
default raw subscriber, token none ends with backends:cpu:
default raw subscriber, keys base key base key and group key
default raw, buffer-aware publisher receives nothing receives
default raw, sample for another subscriber not delivered not delivered, unchanged
raw subscriber with Off stays out stays out, unchanged
transient-local or custom key format stays out stays out, unchanged
raw action client feedback stays out stays out, unchanged
raw.inner.undeclare() (feature ffi) compiles; stops one subscription does not compile; use raw.undeclare()

Compatibility

change who is affected action
RawSubscriber.inner is no longer public Rust code with the ffi feature that calls raw.inner.undeclare() (the field is public in release 0.2.0) call raw.undeclare()
raw subscriber joins by default subscribers behind a default-deny router allow both keys; see below
raw subscriber joins by default every raw, C and Go subscriber, on every topic none; use create_raw_subscriber_with_group with Off to stay out (Rust only)

A router that allows only the base key starves a joined raw, C or Go subscriber of a buffer-aware publisher, with no error. Allow both keys. BufferGroup::Off needs no group key. A subscriber with no backends: field gets no buffer message from a buffer-aware publisher. This holds until a release carries ros2/rmw_zenoh#1078, so Off does not bring those messages back.

Design

A raw subscriber has no message schema, so Auto and Cpu do the same for it. A subscriber with no schema joins on every topic, not only on buffer topics. The subscriptions come before the token, as in #384. The token drops first. This sets the order of local undeclaration only. A remote publisher can still count the subscriber, and send to it, for a short time after the drop. The typed subscriber and the cache drop their handles in a different order; this PR leaves them unchanged.

Tests

The raw suite plays the publisher with a plain Zenoh session, so it needs no ROS installation. CI runs it in its own cargo nextest run --no-tests=fail step, so a build without the raw-sub-tests feature fails instead of passing with zero tests.

test (buffer_cpu_group_raw.rs) checks
raw_subscriber_default_joins_and_announces_backends the default constructors declare a backends:cpu: token, and one callback serves both keys
raw_subscribers_that_stay_out_announce_no_backends_and_skip_the_group_key Off, transient-local QoS and a custom key format declare no token and read only the base key
raw_action_client_feedback_subscriber_stays_out the feedback subscriber of a raw action client stays out
raw_subscriber_declares_two_exact_subscriptions_and_off_declares_one two exact subscriptions when joined, one for Off
raw_group_subscription_is_declared_before_the_token_is_visible the group subscription exists before the token
raw_access_control_starves_the_group_key_silently a router that denies the group key drops buffer messages with no error
raw_denied_group_key_leaves_the_base_key_flowing the same router still delivers the base key
raw_create_and_teardown_cycles_leave_no_token_and_no_callback undeclare() and drop remove the token and both callbacks
raw_join_logs_both_keys_at_debug_level a join logs one debug line with both keys, and an Off subscriber logs none
test (buffer_cpu_group_raw_interop.rs) checks
raw_default_subscriber_receives_from_a_released_publisher a default raw subscriber receives every sample from a released rmw_zenoh_cpp publisher, beside a released subscriber and an Off raw subscriber; the Off subscriber receives none. The released subscriber can make the publisher write the group key alone, so this cell does not show that the raw token matched
raw_default_subscriber_alone_matches_a_released_publisher the default raw subscriber is the only subscriber: its token alone makes the publisher match and write the group key, and it receives every sample

The interop cells publish with ros2 topic pub and count what arrives. They check totals, not per-message identity.

To check that the tests can fail, I added one deliberate bug at a time to the production code. Each bug turned at least one raw test red.

deliberate bug what it breaks
teardown forgets the token undeclare() leaves the backends:cpu: token declared
teardown forgets the group subscription undeclare() leaves the group-key callback running
create_raw_subscriber or _with_qos pass Off the default raw subscriber stops joining
group subscription declared after the token a publisher can see the token before the group subscription exists
raw path ignores Off, transient-local, custom key format or the feedback case a subscriber that must stay out joins
raw join does not log its two keys no debug line for a raw, C or Go join

#384 has merged. Merge order: this PR, #388, #389, then #386.

The three mutant tests failed through a plain assert. They now fail through r1_assert with their own name, so a failure carries the R1ASSERT marker.
A joined typed subscriber that is dropped must stop its callback on the group key. No test owned the mutant that forgets the group subscription on drop. The callback runs on the group key before the drop, then ten puts after it add none.
A rule on the group key stops a joined subscriber from receiving buffer messages only. A message without a buffer arrives on the base key. Off avoids the join but does not restore buffer messages from a released publisher.
A raw subscriber that joins the group owns three handles: the token, the base subscription and the group subscription. Tearing down by hand must remove all three while the binding is still in scope. The test tears down through the public inner field, so it is red until the wrapper has a consuming teardown.
RawSubscriber made inner public next to the token and the group subscription. A caller that undeclared inner stopped the base subscription only: the group key kept calling back and the token stayed visible. The handles are private now, and undeclare consumes the wrapper: token first, then both subscriptions. The teardown test calls it.
create_raw_subscriber_inner only forwarded from create_raw_subscriber_with_group. Its body now lives in the public constructor, and the two legacy constructors call that constructor directly.
Each raw test built its own router, plain session, context and node, and repeated the token, delivery and teardown checks. A fixture and four assertion helpers now carry them. The tests that said opt-in say explicit Cpu selection, because joining is the default. Every policy case, QoS and key format exclusion, ACL control, exact-key check and lifecycle check stays.
The mixed native cell has a released subscriber next to the raw ones, and that subscriber can make the publisher write the group key by itself. The new cell has the default raw subscriber alone. It requires one matched subscriber in the publisher report, every sample on the group key and none on the base key, and every sample received once. The two cells share one fixture and the same checks. This cell needs a buffer-aware rmw_zenoh_cpp, so only the Lyrical interop job runs it.
The typed suite declared a peer session, a node and 30 matching listeners inline to prove that the group subscription exists when the token shows. The check now lives in the shared test helpers and takes the subscriber builder, so the raw suite can run it too. The typed test calls it and asserts the same thing.
The raw suite checked that the token shows after the subscription only through an ACL wait. It had no test that a publisher reading the token finds the group subscription. The raw path declares both subscriptions before the token, and a change that declares the token first now fails this test.
Clippy with the raw-sub-tests and ros-interop features flags the borrow of a formatted node name in the sole raw interop cell. The CI clippy step does not enable raw-sub-tests, so it never lints this file.
@YuanYuYuan YuanYuYuan changed the title feat(hiroz): let a raw sub join the cpu group feat(hiroz)!: let a raw sub join the cpu group Oct 22, 2026
@YuanYuYuan
YuanYuYuan merged commit d315b41 into main Oct 23, 2026
61 of 62 checks passed
@YuanYuYuan
YuanYuYuan deleted the feat/raw-subscriber-buffer-group-3 branch October 23, 2026 17:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants