Repository navigation
fix(rtps): deliver SPDP DATA with readerId ENTITYID_UNKNOWN to the SPDP reader - #871
Conversation
…DP reader A DATA submessage may be addressed to the destination participant instead of to one reader (readerId ENTITYID_UNKNOWN, RTPS 2.5 8.3.7.2). The receiver then finds the reader through the writer it is matched to. That works for SEDP and user data, whose readers hold a proxy per remote writer, but not for SPDP: the SPDP reader is best-effort and never matches remote SPDP writers, so such a DATA found no reader and was dropped. CycloneDDS sends its periodic participant announcements exactly this way; only its first message to a newly seen peer names the SPDP reader. So discovery succeeded, but no later announcement reached SPDPAgent, the peer's liveliness was never refreshed, and checkAndResetHeartbeats() removed it once its lease (10 s for Cyclone) had passed, dropping every matched endpoint with it. On a RAMMP MIB (ESP32-P4) against a ROS 2 Humble host, every rmw_cyclonedds peer died 10 to 16 s after discovery and never came back, since Cyclone did not know it had been forgotten and never resent its SEDP. A longer lease on the Cyclone side only delayed it. Route a DATA from ENTITYID_SPDP_BUILTIN_PARTICIPANT_WRITER with an unknown reader to ENTITYID_SPDP_BUILTIN_PARTICIPANT_READER. No wire change. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
✅Static analysis result - no issues found! ✅ |
Every reader and writer, the two SEDP discovery endpoints included, holds a pool of remote proxies whose depth on the esp32 profile is 6. Each remote participant takes one slot in each SEDP writer and reader, so the MIB completed discovery with six peers and silently skipped the rest. On the bench, the seventh ROS 2 node never saw the MIB's endpoints. 24 covers the Jetson's graph with room; about 60 bytes a proxy. The two remaining limits are espp's: the remote-participant pool of 10 (esp-cpp/espp#870 adds the knob) and CycloneDDS peers expiring after their lease because their repeated SPDP is dropped (esp-cpp/espp#871). Until those ship, a Jetson with more than ten DDS participants loses the MIB after about ten seconds. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
There was a problem hiding this comment.
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Copilot review overview
2 open issues
What changed in this PR
Fixes RTPS discovery interop with CycloneDDS by correctly delivering SPDP DATA submessages addressed to the destination participant (readerId ENTITYID_UNKNOWN) to the SPDP reader, preventing peers from expiring due to missed periodic announcements.
Changes:
- Special-cases SPDP participant writer DATA with unknown readerId to route directly to the SPDP builtin participant reader.
- Retains the existing matched-writer lookup path for all other writers.
| File | Description |
|---|---|
| components/rtps/src/messages/MessageReceiver.cpp | Routes SPDP DATA with ENTITYID_UNKNOWN readerId to the SPDP reader instead of relying on matched-writer proxy lookup. |
Review effort: Lite
| if (dataSubmsg.writerId == ENTITYID_SPDP_BUILTIN_PARTICIPANT_WRITER) { | ||
| // SPDP DATA addressed to the participant rather than to our SPDP reader | ||
| // (readerId ENTITYID_UNKNOWN, RTPS 2.5 8.3.7.2): CycloneDDS sends its | ||
| // periodic announcements this way, and only its first message to a newly | ||
| // seen peer names the reader. The SPDP reader is best-effort and matches | ||
| // no remote writer, so the proxy lookup below never finds it and the | ||
| // repeat was dropped: the peer's liveliness was never refreshed and it | ||
| // expired after its lease, taking every matched endpoint with it. | ||
| reader = mp_part->getReader(ENTITYID_SPDP_BUILTIN_PARTICIPANT_READER, readerGen); | ||
| } else { | ||
| reader = mp_part->getReaderByWriterId( | ||
| Guid_t{sourceState.sourceGuidPrefix, dataSubmsg.writerId}, readerGen); | ||
| } |
There was a problem hiding this comment.
Added in 2d5d1c0: pc/tests/rtps_spdp_unknown_reader.cpp, a standalone host test (picked up by the pc/tests glob and by host_sanitizers.yml, no Docker or peer). It hands crafted datagrams to a participant through Participant::newMessage(): (1) an announcement naming the SPDP reader adds the peer, the control; (2) a re-announcement with readerId ENTITYID_UNKNOWN and the same sequence number, what CycloneDDS sends, must refresh its liveliness; (3) a first announcement with an unnamed reader is discovered too; (4) an unnamed-reader DATA from a non-SPDP writer is still dropped, so the routing stays scoped to the SPDP writer. Verified it fails on upstream's MessageReceiver.cpp at check 2 (FAIL: re-announcement with readerId ENTITYID_UNKNOWN refreshes liveliness) and passes with the fix. #874 covers the same class end to end against a real CycloneDDS peer.
| // SPDP DATA addressed to the participant rather than to our SPDP reader | ||
| // (readerId ENTITYID_UNKNOWN, RTPS 2.5 8.3.7.2): CycloneDDS sends its | ||
| // periodic announcements this way, and only its first message to a newly | ||
| // seen peer names the reader. The SPDP reader is best-effort and matches | ||
| // no remote writer, so the proxy lookup below never finds it and the | ||
| // repeat was dropped: the peer's liveliness was never refreshed and it | ||
| // expired after its lease, taking every matched endpoint with it. |
There was a problem hiding this comment.
Trimmed in 2d5d1c0 to the rationale and the spec citation (RTPS 2.5 8.3.7.2), plus a pointer to the regression test. The CycloneDDS behaviour and the lease-expiry chain stay in the PR description and the commit message.
…OWN; trim the receiver comment pc/tests/rtps_spdp_unknown_reader.cpp hands crafted datagrams to a participant through Participant::newMessage(), no network: an announcement naming the SPDP reader adds the peer; a re-announcement with readerId ENTITYID_UNKNOWN and the same sequence number (what CycloneDDS sends) must refresh its liveliness, which fails without the fix; a first announcement with an unnamed reader is discovered too; and an unnamed-reader DATA from a non-SPDP writer is still dropped, so the routing stays scoped to the SPDP writer. It is picked up by the pc/tests glob and runs standalone in host_sanitizers.yml. The inline comment in MessageReceiver.cpp now carries only the rationale and the spec citation and points at the test; the failure story stays in the PR. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…d the CycloneDDS addressing fixes (#874) * test(rtps): interop cases for lease liveliness and a busy domain, on FastDDS and CycloneDDS Two defect classes found against CycloneDDS peers had no coverage because every ROS 2 pair in the matrix proves a single sample within a few seconds: - Liveliness past the remote participant lease. A peer whose SPDP re-announcements are not delivered to the SPDP reader has its lease expire (10 s for CycloneDDS, which addresses the repeats to readerId ENTITYID_UNKNOWN) and loses every matched endpoint - discovery looked fine, data stopped after the lease. Both legs now run ~30 s and count only samples that arrive after 16 s: `rtps_interop_sub` gains a backward-compatible `count_after_s` argument, and a small rclpy `ros2_count_subscriber.py` does the same on the ROS 2 side. Run on the FastDDS pass as a control and on the CycloneDDS pass, where it fails until the SPDP-reader delivery fix (#871) is in. - A busy domain. Twelve unrelated ROS 2 participants are up before espp starts, then the publisher espp needs joins last and must still be matched: a participant beyond the remote-participant pool is ignored silently. The harness builds the host profile (32 slots) so this passes; the embedded profile's 10 slots need the pool raised for such a domain (#870's knob), which the note and README say. A live multi-homed-peer case (the #865 class) is described in the README as follow-up work: it needs a peer address espp cannot reach, which one shared network namespace cannot provide. Workflow timeout 30 -> 40 min for the ~3 min the four new runs add. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DFjjnq3XCTRAXENSxsCxJU * test(rtps): interop liveliness window past the worst-case expiry; lift CycloneDDS's participant-index cap Count from 22 s in, not 16: discovery, the 10 s lease and the liveliness check cadence all fit before that, so a peer dropped at its lease cannot contribute to the count. CycloneDDS allows ten participants per host per domain by default (MaxAutoParticipantIndex 9); the busy-domain case creates thirteen, so the CycloneDDS pass raises it through CYCLONEDDS_URI. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DFjjnq3XCTRAXENSxsCxJU * test(rtps): interop review follow-ups; note why only the espp-publisher liveliness leg detects a dropped peer - cleanup `wait`s in the new cases tolerate an already-reaped PID - rtps_interop_sub: count_after_s < 0 is a usage error (exit 1); early samples are verified byte-exact and reported like counted ones, only the count differs - ros2_count_subscriber.py: argparse with typed, non-negative arguments - README: CI showed only `espp_pub->ros2_sub_liveliness` failing on CycloneDDS. A HEARTBEAT reaching a matched espp reader refreshes the sending participant's liveliness, so a CycloneDDS writer keeps its participant alive through its heartbeats even with its SPDP re-announcements lost; an espp writer only gets ACKNACKs, which do not, and the peer expires. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DFjjnq3XCTRAXENSxsCxJU --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
espp 1.4.0 ships esp-cpp/espp#871, so a CycloneDDS peer's repeated announcements reach the MIB and it no longer drops the Jetson's nodes 10 s after discovery, and #870, so the remote-participant pool is a knob: 32 here, beside the history and proxy caps from #61. The manifest's floor moves to 1.4.0 and the other espp components follow it. docs/ros2.md drops the "until the release" wording. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>


Summary
A DATA submessage may be addressed to the destination participant instead of to one reader (readerId
ENTITYID_UNKNOWN, RTPS 2.5 §8.3.7.2).MessageReceiver::processDataSubmessage()then resolves the reader through the writer it is matched to. That works for SEDP and user data, whose readers hold a proxy per remote writer, but not for SPDP: the SPDP reader is best-effort and never matches remote SPDP writers, so such a DATA found no reader and was dropped.CycloneDDS sends its periodic participant announcements exactly this way; only its first message to a newly seen peer names the SPDP reader (
0:0:0:100c7), and the repeats reuse sequence number 1. So discovery succeeded, but no later announcement reachedSPDPAgent, the peer's liveliness was never refreshed, andcheckAndResetHeartbeats()removed it once its advertised lease (10 s for Cyclone) had passed, dropping every matched endpoint with it. Cyclone did not know it had been forgotten and never resent its SEDP, so the peer never came back. A longer lease on the Cyclone side only delayed it.MessageReceiver: a DATA fromENTITYID_SPDP_BUILTIN_PARTICIPANT_WRITERwith an unknown reader is delivered toENTITYID_SPDP_BUILTIN_PARTICIPANT_READER. Everything else keeps the matched-writer lookup.Regression test (review follow-up, 2d5d1c0):
pc/tests/rtps_spdp_unknown_reader.cpp, standalone, no peer. It hands crafted datagrams to a participant throughParticipant::newMessage(): an announcement naming the SPDP reader adds the peer; a re-announcement withreaderId ENTITYID_UNKNOWNand the same sequence number must refresh its liveliness (fails onmainat exactly that check); a first announcement with an unnamed reader is discovered too; an unnamed-reader DATA from a non-SPDP writer is still dropped. Runs underhost_sanitizers.ymlwith the otherrtps_*tests. test(rtps): interop cases for lease liveliness, many participants, and the CycloneDDS addressing fixes #874 covers the same class against a real CycloneDDS peer.No wire-format change. Independent of #870 (the participant-pool knob); the two were found together on the same bench.
Test plan
rmw_cycloneddsparticipants: before, every Cyclone peer's MibStatus stream stopped 10–16 s after it started (seq frozen, Cyclone trace showed the MIB's SEDP going unanswered afterwards); after, a peer on the default 10 s lease stayed matched for the whole 90 s test run, with 2 Hz status in and DriveCommand/TwistSetpoint out0:0:0:0and seq#1; the initial ones with…:100c7pc/tests/rtps_spdp_unknown_readerpasses with the fix and fails onmain'sMessageReceiver.cpp(FAIL: re-announcement with readerId ENTITYID_UNKNOWN refreshes liveliness);rtps_pubsub,rtps_locator_subnetandrtps_sedp_dedicated_locatorstill pass on the host buildpc/tests/rtps_interop_*) not run here🤖 Generated with Claude Code