Skip to content

feat: explicit subscriber durability for latched topics - #2

Merged
karmanyaahm merged 4 commits into
mainfrom
feat/subscriber-durability
Jun 19, 2026
Merged

feat: explicit subscriber durability for latched topics#2
karmanyaahm merged 4 commits into
mainfrom
feat/subscriber-durability

Conversation

@karmanyaahm

@karmanyaahm karmanyaahm commented Jun 19, 2026

Copy link
Copy Markdown
Collaborator

What

Adds explicit subscriber QoS durability so clients can opt into latched (transient_local) delivery, fixing a discovery race where a subscriber that connects before the publisher is discovered is created volatile and silently misses the publisher's retained sample.

A subscribe message may include a qos object (per the rosbridge QoS spec):

{ "op": "subscribe", "topic": "/latched_topic", "type": "std_msgs/msg/String", "qos": { "durability": "transient_local" } }
qos.durability Behavior
"transient_local" Request latched delivery; receive the retained sample even if subscribing before the publisher is discovered.
"volatile" Force volatile delivery (no retained samples).
(omitted) Auto-match an already-discovered publisher's durability (legacy behavior, unchanged).

Only qos.durability is honored; other QoS fields (reliability/deadline/lifespan/best_available) are not yet supported.

Why

The previous behavior derived subscriber durability from get_publishers_info_by_topic at subscribe time. If the publisher wasn't discovered yet, the subscriber was created volatile and missed the latched sample. Letting the client declare durability explicitly skips the discovery query entirely and is race-free.

Changes

  • connector.hpp: durability field on topic_params; applied directly when set, else legacy auto-match.
  • client_handler.cpp: parse qos.durability from the subscribe message.
  • README.md: documents the qos.durability field.

Tests

  • connector_test.cpp: deterministic mock tests reproducing the race both ways (with/without explicit durability), asserting discovery is not consulted when durability is explicit.
  • client_handler_test.cpp: real-ROS e2e — transient_local subscriber receives a pre-published latched sample; volatile subscriber provably does not.
  • test_durability.py: black-box test over a real WebSocket against rws_server (no rws C++ linkage), manually observable.

All gtest suites pass; pep257 clean.

🤖 Generated with Claude Code

karmanyaahm and others added 4 commits June 18, 2026 18:10
Add an optional `durability` field to the `subscribe` op so clients can
request `transient_local` (or `volatile`) QoS directly, instead of relying
on rws auto-matching the publisher's durability at subscribe time.

Auto-matching reads `get_publishers_info_by_topic` when the subscription is
created. If the latched publisher has not been discovered yet, the subscriber
is created volatile and silently misses the publisher's retained sample — a
discovery race. A `transient_local` subscriber is QoS-compatible with a
transient_local publisher and receives the retained sample whenever they
match, regardless of which side started first, with no dependency on
discovery timing.

- topic_params: add `durability` (default SystemDefault = legacy auto-match)
  and include it in equality so subscribers with different durability stay
  distinct.
- Connector::subscribe_to_topic: when durability is explicit, apply it
  directly and skip the publisher-info query; otherwise keep auto-matching.
- client_handler: parse the optional `durability` string on subscribe.

Tests:
- connector_test: deterministic race reproduction via mocked discovery state
  (volatile when publisher undiscovered without the param; transient_local
  with the param even when undiscovered; legacy auto-match still works;
  explicit volatile overrides; params differing only in durability are
  distinct).
- client_handler_test: end-to-end real-ROS coverage — a transient_local
  subscriber receives a sample latched before it subscribed, while a volatile
  subscriber matches the same publisher but does not.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
End-to-end test that talks to a running rws_server over a real WebSocket
(no linkage to rws C++). A latched rclpy publisher publishes a retained
sample before any subscriber connects, then the test asserts a
transient_local subscriber receives it while a volatile subscriber does not.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Use single-line docstrings + comments so ament_pep257 (run in CI) passes.
- Comment the dependencies and RMW requirement at the top.
- Print what is published/received so the behavior is observable manually.
- Drop the rclpy.spin thread: the middleware retains the latched sample for
  late joiners without spinning, which also removes noisy shutdown output.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move the explicit subscriber durability field from a top-level "durability"
key to the spec-conformant {"qos": {"durability": "..."}} location. Only
qos.durability is honored; other QoS fields are not yet supported.

Implementation is otherwise unchanged: transient_local / volatile map to the
same DurabilityPolicy, omitted falls back to publisher auto-match. Updates
README, the C++ e2e tests, and the black-box Python test to the new format.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@karmanyaahm
karmanyaahm merged commit 3b7d582 into main Jun 19, 2026
1 check passed
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.

1 participant