Skip to content

feat(hiroz): join the rmw_zenoh cpu buffer group - #384

Merged
YuanYuYuan merged 138 commits into
mainfrom
feat/buffer-group-typed-join
Oct 21, 2026
Merged

YuanYuYuan merged 138 commits into
mainfrom
feat/buffer-group-typed-join

Conversation

@YuanYuYuan

@YuanYuYuan YuanYuYuan commented Oct 16, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

A buffer-aware rmw_zenoh publisher changes its keys for a message with an unbounded uint8[] or char[] field, such as Image. It stops writing the plain topic key. It writes <topic key>/_buf_cpu for each subscriber whose token ends with backends:cpu:. See ros2/rmw_zenoh#930 and ros2/rmw_zenoh#987. A hiroz subscriber announces no backends and reads only the base key. It receives nothing from such a publisher, with no error and no log.

Fixes #371. Part of #370.

What changes

  • New public API: the #[non_exhaustive] enum BufferGroup { Auto, Cpu, Off }. The subscriber builder and the cache builder get with_buffer_group. The default is Auto.
  • A joined subscriber ends its token with backends:cpu:. It declares two exact subscriptions, the base key and the group key (<topic key>/_buf_cpu), before the token.
  • Off keeps the token and the single base subscription of main. rmw-zenoh-rs passes Off.
  • joins and declare_token in buffer_group.rs replace the earlier Plan type. The builder wrapper logs one debug event per join.
setting the subscriber joins when
Auto (default) the message has an unbounded uint8[] or char array
Auto, no definition hiroz has no message definition to ask
Cpu always, except as noted below
Off never

A nested field counts. The typed code generator also counts byte[]: this costs one extra join and loses no data. Bounded arrays, fixed arrays and int8[] do not count. Transient-local subscribers never join. Subscribers on other key formats than rmw_zenoh never join.

Before and after

main joined subscriber
subscriptions base key base key and group key
token ends after the QoS chunk ends in backends:cpu:
router rule must allow base key base key and group key
message from a buffer-aware publisher not delivered delivered on the group key
message from another publisher delivered on the base key delivered on the base key

Compatibility

change who is affected action
A joined subscriber also reads the group key users behind a default-deny router allow both keys
Python and action-client feedback subscribers join the same users allow both keys
A subscriber with no definition joins on every topic the same users the same action
Lost-message events when other accelerated subscribers share the topic joined subscribers on such a topic none in hiroz; see Limits

Off stops a subscriber from joining, so it needs no rule for the group key. It does not bring back buffer messages: a subscriber with no backends: field receives none from a buffer-aware publisher until a release carries ros2/rmw_zenoh#1078.

A rule on <topic key>/** is not needed. With a default-deny router, build returns Ok and the subscriber receives nothing from a buffer-aware publisher.

Design

choice reason evidence
Two exact subscriptions, no wide key same keys as rmw_zenoh_cpp; narrow router rule T1, T2
Keep the base key publishers that are not buffer-aware write only there T3
Subscriptions before the token a publisher that reads the token must find them T4
Transient-local never joins the released package serves history on the base key only none
  • T1: router_access_control_starves_a_joined_subscriber_silently pins the router rule on the two keys.
  • T2: sibling_sample_key_has_no_subscription_and_its_samples_never_reach_a_joined_subscriber shows that the process subscribes to no wide key.
  • T3: joined_subscriber_reads_the_group_key_with_its_locality and the rows of cache_join_policy pin delivery on both keys.
  • T4: group_subscription_is_declared_before_the_token_is_visible pins the declaration order on the typed path.

Transient-local measurement on the released 0.10.6 package: n=5 per cell, five kinds of late joiner, 75 Image cells. History arrived on the base key in every cell and never on the group key. An unreleased revision serves it on the group key. No test covers history after ros2/rmw_zenoh#1078.

Declaration cost, from an earlier experimental build on one machine, n=5, N=1000 subscribers: two exact subscriptions took 3.2 times the declaration time of main, and one wide key took 1.2 times. This head has not repeated it.

Tests

The head has 12 router tests, 4 interop tests, 2 router-free join tests and 1 codegen test. The router tests cover join policy, token, delivery, teardown, the debug log and router access control. The router-free tests and the codegen test cover which field kinds join.

  • Join policy: typed_subscriber_join_policy, dynamic_subscriber_join_policy, cache_join_policy and action_client_join_policy.
  • Teardown: dropping_a_joined_typed_subscriber_removes_its_token_and_stops_callbacks and dropping_a_joined_cache_stops_group_key_updates.
  • Field kinds: dynamic_schema_shapes_decide_the_join and typed_message_fields_decide_the_join in buffer_group_join.rs, and byte_char_and_uint8_sequences_share_one_schema_kind in the codegen crate.

Evidence

check result conditions
12 router, 2 join, 1 codegen tests 15 of 15 pass in each mode remote Linux machine, previous head, clean copy; nextest, cargo test
12 router tests after the docs-only commit 12 of 12 pass same machine, nextest
reduced test file on the earlier wide head 2 of 12 fail same machine; T1 and T2 fail
9 mutants, such as no join and wide key each turns at least one retained test red previous head, one run each, n=1
clippy and mkdocs strict clean previous head, same machine
cargo doc no unresolved links previous head, same machine
cargo check --tests exit 0 humble, kilted, lyrical and jazzy features, previous head
4 interop tests on a Lyrical image all pass, none can skip previous head; see the note below
CI at this head 37 success, 2 skipped, 0 pending, 0 failed read 21 October 2026, 23:12 UTC+8

The 2 skipped jobs are Publish to GitHub Pages. The interop tests ran in this CI job with HIROZ_REQUIRE_BUFFER_RMW=1, which forbids a skip: https://github.com/ZettaScaleLabs/hiroz/actions/runs/39590732929/job/119190731664. The token-before-subscriptions mutant covers the typed path only.

Limits

  • Lost-message events: the publisher advances one sequence number for each endpoint copy. Measured with K other accelerated subscribers (K=1 and K=4), n=5, 100 messages per second for 20 s, one machine. The released 0.10.6 package and an unreleased revision gave a lost count of K times the delivered count. With ros2/rmw_zenoh#1079 the count was 0. Verify by: rerun with a release that carries that fix.
  • Accelerated subscribers: no test uses a real accelerated subscriber. Verify by: run one against a buffer-aware publisher.
  • Declaration cost: measured on an earlier build, not at this head. A joined subscriber with no definition declares both subscriptions on every topic. Verify by: repeat with the example router.
  • Transient-local: no test covers history after ros2/rmw_zenoh#1078. Verify by: run a transient-local late joiner against a release that carries it.

This PR builds on main. Merge order: #384 (this PR), #385, #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.
Add the exact-join tests: delivery under a rule that allows only the
base and group keys, the sibling sample key that no subscription
matches, subscription and token counts, a typed topic with no buffer
field, a transient-local subscriber, a typed subscriber with no schema,
and the join decision per field kind.

Each test pairs its negative check with a positive control. The field
kind table pins that the typed path counts byte[]. ros-msgs now enables
dynamic-schema-loader so the table can read load_schema.
Cite the rmw_zenoh_cpp code the join copies, state the measured buffer field rule, drop the claim that transient-local history uses the base key only, and label the guide sentences that were read from source and not measured.
The plan held the topic key that both builders already hold, and it forced a call order that no text stated. Two functions, group_key and declare_token, give the same behaviour with less code.
The regrouping was formatter output and has nothing to do with the join. The file keeps the layout of main, and the token field uses the LivelinessToken import again.
Two tests repeat rows float_auto and tl_default of typed_subscriber_join_policy. A third repeated the matching-status probe of the sibling key test; its three extra keys now sit in that test. The test of the key that a real publisher writes stays: it is the only one that checks decoded data on the group key.
The rule sits in BufferGroup. The method doc restated it and could drift from it, so it now gives one sentence and a link.
A tuple alias hid what the pair owns. The struct states that it holds the topic-key subscription and the optional group subscription, and that dropping it undeclares both. Field order keeps the drop order of the tuple.
The join declares two exact subscriptions. Name the probes on the topic key and everything under it as controls, and drop the comparison with the wide shape from a test doc and the guide.
The field-kind test loads std_msgs definitions through load_schema. The humble build embeds no std_msgs text, so the test panicked there. The loaded rows now exist only without the humble feature. The typed rows run on every distro.
A transient-local subscriber stays out of the group because the released package serves its history on the base key only. The guide now says what was measured and what was not. A joined subscriber can report lost-message events when other buffer-aware subscribers share the topic. The guide lists this as a limit and notes the interop tests do not cover it.
The policy predicate is pure now. The builder logs one debug line per join and reuses the group key it already built. ZSub and ZCache keep the field order and names of main, because nothing observes drop order.
Private comments no longer carry run counts, setups or revision history. The user guide holds the evidence.
Tables hold the field rule and the join setting. The lost-message limit appears once. The cases that never join form a list.
The depth, occupancy grid, volatile action and loaded-schema rows pinned code that another row pins. The schema-loader feature of hiroz-tests has no user left.
Every joined table row now proves the base key and decodes a nonempty payload, so the cache two-key test, the real-publisher test and the schemaless Num test have no unique check left.
One table now holds the base-only rule and the exact-pair rule. The default-allow deny rule and the wildcard probes of the old design are gone. A wider subscription still turns the pair rows red.
The per-gid key stays as the one negative, with the base and group keys as positives and one broad observer as the control. Three keys that no design proposes are gone.
The join event carries the base key and the group key as fields. The test checks the level, the keys and one event per join, and no longer freezes the English text.
The schema matrix and the field-kind matrix need no router. They read the token of a subscriber on one session. A codegen test pins that char[], byte[] and uint8[] share one schema kind. The router tests keep two dynamic rows with delivery checks.
raw_session had the body of buffer_rmw::client_session.
The plain-topic test repeats the action feedback test, which stays. The group-key counter of that test had no positive control.
The mixed-set test required zero base-key samples and zero delivery to the Off subscriber. A publisher with the fix for legacy subscribers can break both and still deliver every joined message. The guide states the starvation as a limit.
The module comment no longer states the starvation of legacy subscribers as a fact of every publisher.
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.
@YuanYuYuan YuanYuYuan changed the title feat(hiroz): join the rmw_zenoh cpu buffer group by default feat(hiroz): join the rmw_zenoh cpu buffer group Oct 21, 2026
@YuanYuYuan
YuanYuYuan merged commit 1615027 into main Oct 21, 2026
40 checks passed
@YuanYuYuan
YuanYuYuan deleted the feat/buffer-group-typed-join branch October 21, 2026 15:14
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.

hiroz typed and dynamic subscribers receive nothing from buffer-aware rmw_zenoh publishers

2 participants