fix(event): raise MessageLost from sequence gaps - #294
Open
YuanYuYuan wants to merge 5 commits into
Open
Conversation
YuanYuYuan
force-pushed
the
fix/message-lost-event
branch
from
August 6, 2026 10:32
3c4894b to
604b0cd
Compare
RMW_EVENT_MESSAGE_LOST was fully plumbed and never raised: the enum, the rmw_event_type mapping, the rmw_message_lost_status_t fill-in and the Attachment::sequence_number on the wire all existed, but nothing emitted it. A subscriber asking for the event got a callback that never fired and a status permanently zero (#292). MessageLossTracker holds the last sequence seen per publisher GID and raises the event when an arrival skips past one. It is the only place hiroz trailed rmw_zenoh_cpp on event coverage. Deliberately not counted: a subscriber dropping its own oldest queued sample at the history depth. That sample arrived and updated the baseline, so it produces no gap -- and upstream draws the line in the same place, logging depth-drops at debug and raising the event only for gaps. Raises via update_shared_event_status, so the callout happens with no lock held (#259/#260); the per-GID map has its own lock and is never held across it. Depends on #260 for that entry point -- it does not exist on main.
A replayed or reordered sample must not move the high-water mark backwards, or the next ordinary sample reads as a gap. This diverges from rmw_zenoh_cpp deliberately. Upstream uses std::abs(sn - last) and rewrites the baseline unconditionally, so on arrivals 5, 3, 6 it reports 1 lost for the replay and 2 more for the sample after it. Every TransientLocal subscriber replays history, so that false positive is reachable rather than theoretical.
The unit tests in event.rs exercise MessageLossTracker directly, so deleting the observe_loss(..) call from the subscriber receive path leaves every one of them green. This file fails in that case. Loss is induced deterministically instead of by dropping a packet: the test publishes onto the subscriber's own key expression through the node's session with a hand-built Attachment, so the sequence gap is exact and there is no timing to lose.
Both accessors lived on the ZSub<T, Sample, S> (queue-mode) impl, so a callback subscriber could not reach its own events manager -- the handle the rmw layer needs to install an event callback, and the only way to observe MessageLost. Neither field has anything to do with the queue. Moving them to a generic impl is what let the wiring test observe a callback subscriber's loss counter at all.
ZSub declares T: ZMessage, S: ZDeserializer on the struct, so a bare impl<T, Q, S> does not satisfy them.
YuanYuYuan
force-pushed
the
fix/message-lost-event
branch
from
August 14, 2026 18:23
604b0cd to
5ed2b56
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
hiroz plumbs
RMW_EVENT_MESSAGE_LOSTfully, but raises it nowhere. A subscriber that registers a callback for it gets one that cannot fire, plus a status that stays zero. In-transit message loss is invisible to every ROS 2 application on hiroz.This PR adds
MessageLossTracker: a per-publisher sequence-number baseline on the subscriber. It raises the event when an arrival skips past that baseline.Fixes #292.
The defect
What already existed, and the one piece that was missing:
ZenohEventType::MessageLostevent.rsrmw_event_type3 →MessageLostrmw_event_type_is_supportedreturns truermw_message_lost_status_tfill-in onrmw_take_eventAttachment::sequence_numberandsource_gidon every sample#[cfg(test)]ZenohEventTypehas 11 variants. Four map toNoneand are honestly declared unsupported: bothLIVELINESS_*and bothDEADLINE_MISSED. Four are raised fromrmw.rs. Three are declared supported and never raised.This PR closes the one where hiroz trails
rmw_zenoh_cpp. The other two areSUBSCRIPTION_INCOMPATIBLE_TYPEandPUBLISHER_INCOMPATIBLE_TYPE, tracked as #293, whichrmw_zenoh_cppdoes not raise either.What this PR does
MessageLossTracker— per-GID baseline, raisesMessageLoston a forward skipcrates/hiroz/src/event.rsobserve_loss(..)on the subscriber receive path, insidebuild_internalso every build variant is coveredcrates/hiroz/src/pubsub.rsevents_mgr()andentity()widened from the queue-mode impl to allZSubvariantscrates/hiroz/src/pubsub.rsevent.rs,crates/hiroz-tests/tests/message_lost.rsThe tracker ignores a sample whose attachment is missing or undecodable. A plain zenoh peer sends no sequence number. hiroz must not treat such a peer as lossy just because it looks unfamiliar.
Alignment with
rmw_zenoh_cppThe reference is
SubscriptionData::add_new_message, upstream commite3159856e3816e726f2c5f1f9b4858ad6b7ee63e. This section compares against its gap logic.Matched
rmw_zenoh_cppn - 1i32std::clampmin(i32::MAX)DataHandler::handleincommon.rsThe depth-drop row is easy to get wrong. A sample dropped because the subscriber's queue is full arrived. It advanced the baseline, so it produces no gap. Neither implementation reports it as
MESSAGE_LOST. This PR does not change that.Divergences, deliberate
DV1 — an out-of-order arrival no longer reports phantom loss. Upstream computes
std::abs(sn - last)and rewrites the baseline unconditionally. This PR advances the baseline only forward, and reports nothing for an arrival at or below it.5, 3, 6rmw_zenoh_cppabs(3-5)=2→ 1, thenabs(6-3)=3→ 2. Total 3 phantomThis is the recovery path, not a contrived input. Both implementations enable heartbeat-based miss detection on reliable subscribers:
recovery->last_sample_miss_detection = RecoveryOptions::Heartbeat{}upstream, andRecoveryConfig::default().heartbeat()here. Recovery exists precisely to deliver a missed sample after newer ones have arrived.Note
Upstream behaviour here comes from reading
rmw_subscription_data.cpp, not from executing it. Bothon_sampleclosures feedadd_new_message, and both are installed viadeclare_advanced_subscriber. Live and recovered samples therefore share the same gap logic. The hiroz side is pinned bymessage_loss_survives_a_transient_local_replay.DV2 — no hash collisions between publishers. Upstream keys its map on
hash_gid(...), asize_t. Two publishers whose GIDs collide would interleave their sequence numbers into one baseline, producing continuous phantom loss on both. This PR keys on the full 16-byteGidArray.DV3 — the callout happens with no subscriber lock held. Upstream calls
update_event_statusfrom insideadd_new_message. That method holdsSubscriptionData::mutex_for its whole body — the shape #259 is about.EventsManager::update_event_statusdoes releaseevent_mutex_beforetrigger_event_callback; the subscription mutex is the one still held. In this PR the per-GID map has its own lock, and the guard drops before the call toupdate_shared_event_status.DV4 —
std::clamp's lower bound is not copied. Upstream clamps to[i32::MIN, i32::MAX]. The value isabs(..) - 1guarded byabs(..) > 1, so it cannot be negative. This PR saturates ati32::MAXonly.Evidence
license/clagreen604b0cd54bbb1165683a26a80ed8c15cbedc0b96observe_loss(..)call from the receive path: the six unit tests still pass,a_sequence_gap_raises_message_lostfailsThe second row is the point. The unit tests exercise
MessageLossTrackerdirectly, so they cannot see whether anything calls it. Withoutmessage_lost.rs, deleting the wiring would be a silent, green regression.crates/hiroz/src/event.rshas 20#[test]functions at the head commit, six of them new: ordinary gap, first sample, per-publisher isolation, reorder/republish, transient-local replay, andi32saturation.The two integration tests induce loss deterministically. Each publishes onto the subscriber's own key expression through the node's session with a hand-built
Attachment, so the gap is exact. Only one of the two is a detector:a_sequence_gap_raises_message_lostjoining_late_reports_no_lossBreaking changes
None. Two API surfaces change shape, both additive:
MessageLossTrackerinhiroz::eventZSub::events_mgr()andZSub::entity()moved to the genericimpl<T: ZMessage, Q, S: ZDeserializer>ZSub<T, Sample, S>, and are now also reachable from callback-mode subscribers, which the rmw layer needsImportant
A subscriber that previously saw
total_count == 0forever now sees real counts. That is the purpose of this PR. It changes behaviour for anything asserting on that status.Coverage this does not have
None of these block the fix. They bound what green CI proves.
G3 is a deliberate trade. In ROS a restarted endpoint normally gets a fresh GID, so it lands in the first-sample path instead. The alternative is upstream's behaviour, which mis-reports every replay (DV1).