Skip to main content

zerodds_rtps/
reader_proxy.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! `ReaderProxy` — writer-side state over **one** remote reader.
4//!
5//! DDSI-RTPS 2.5 §8.4.4.11 (stateful writer behavior). The writer keeps
6//! a `ReaderProxy` per matched reader, in which it tracks which
7//! sequence numbers the reader has already acked and which it has
8//! explicitly re-requested.
9//!
10//! A writer currently has only one reader (single-reader
11//! assumption). Still, the data structure is cut so that `Vec<ReaderProxy>`
12//! is possible later.
13
14extern crate alloc;
15use alloc::collections::{BTreeMap, BTreeSet};
16use alloc::vec::Vec;
17
18use crate::wire_types::{FragmentNumber, Guid, Locator, SequenceNumber};
19
20/// Writer-side state for one remote reader.
21#[derive(Debug, Clone)]
22pub struct ReaderProxy {
23    /// GUID of the remote reader endpoint.
24    pub remote_reader_guid: Guid,
25    /// Unicast receive locator(s) of the reader.
26    pub unicast_locators: Vec<Locator>,
27    /// Multicast receive locator(s).
28    pub multicast_locators: Vec<Locator>,
29    /// Reliable kind (always true in WP 1.1).
30    pub is_reliable: bool,
31    /// Highest SN the reader has **already acked**
32    /// (from AckNack.reader_sn_state.bitmap_base - 1).
33    highest_acked_sn: SequenceNumber,
34    /// Highest SN the writer has **already sent** to this reader.
35    highest_sent_sn: SequenceNumber,
36    /// Set of requested SNs from AckNack.bitmap, queued for re-send.
37    requested_changes: BTreeSet<SequenceNumber>,
38    /// Per sample SN: set of missing FragmentNumbers the reader requested via
39    /// NACK_FRAG. For fragment-granular re-sends.
40    requested_fragments: BTreeMap<SequenceNumber, BTreeSet<FragmentNumber>>,
41    /// Spec §8.4.15.6 inactive-reader reclaim: last observed
42    /// reader activity (incoming ACKNACK / NACK_FRAG). The writer
43    /// calls `note_activity(now)` from the ACKNACK path. If
44    /// `now - last_activity > inactive_threshold`, the writer can recognize the
45    /// proxy as a reclaim candidate via `is_inactive`, so that
46    /// strict reliability does not run the cache OOM.
47    last_activity: core::time::Duration,
48    /// XTypes 1.3 §7.6.3.1 — per-reader negotiated wire format.
49    /// Default `XCDR2` (=2). On match the field is set via
50    /// `data_representation::negotiate(writer_offered, reader_accepted, mode)`,
51    /// otherwise it stays default. The encap header on sample-write
52    /// uses `data_representation::encap_for_final_le(this)`.
53    negotiated_data_representation: i16,
54}
55
56impl ReaderProxy {
57    /// Creates a fresh proxy.
58    #[must_use]
59    pub fn new(
60        remote_reader_guid: Guid,
61        unicast_locators: Vec<Locator>,
62        multicast_locators: Vec<Locator>,
63        is_reliable: bool,
64    ) -> Self {
65        Self {
66            remote_reader_guid,
67            unicast_locators,
68            multicast_locators,
69            is_reliable,
70            // Pre-existing state: nothing acked, nothing sent. SN starts
71            // at 1; "0 acked" means "nothing acked" (§8.7.4).
72            highest_acked_sn: SequenceNumber(0),
73            highest_sent_sn: SequenceNumber(0),
74            requested_changes: BTreeSet::new(),
75            requested_fragments: BTreeMap::new(),
76            last_activity: core::time::Duration::ZERO,
77            // Default: XCDR2 (modern). The SEDP match path overwrites
78            // with the negotiated value.
79            negotiated_data_representation: crate::publication_data::data_representation::XCDR2,
80        }
81    }
82
83    /// Sets the negotiated wire format for this reader.
84    /// Called by the DCPS-SEDP match path after `negotiate(...)`.
85    pub fn set_negotiated_data_representation(&mut self, id: i16) {
86        self.negotiated_data_representation = id;
87    }
88
89    /// Returns the negotiated wire format.
90    #[must_use]
91    pub fn negotiated_data_representation(&self) -> i16 {
92        self.negotiated_data_representation
93    }
94
95    /// Spec §8.4.15.6 — marks incoming reader activity (every
96    /// ACKNACK / NACK_FRAG calls this from the receiver path).
97    pub fn note_activity(&mut self, now: core::time::Duration) {
98        self.last_activity = now;
99    }
100
101    /// Spec §8.4.15.6 — `true` if the reader has shown no activity for
102    /// longer than `threshold`. The caller (e.g. ReliableWriter)
103    /// uses this to reclaim the proxy from the `matched_readers` list,
104    /// so that strict reliability does not run the cache OOM.
105    #[must_use]
106    pub fn is_inactive(&self, now: core::time::Duration, threshold: core::time::Duration) -> bool {
107        now.checked_sub(self.last_activity)
108            .is_some_and(|elapsed| elapsed > threshold)
109    }
110
111    /// Returns the last-activity timestamp (diagnosis).
112    #[must_use]
113    pub fn last_activity(&self) -> core::time::Duration {
114        self.last_activity
115    }
116
117    /// Marks samples up to and including `sn` as "no longer
118    /// relevant" for this proxy — both sent and
119    /// acked. Called e.g. for volatile durability when a
120    /// new reader proxy is added: it should not get historic samples,
121    /// so we jump directly to the current cache
122    /// state.
123    ///
124    /// Spec reference: OMG DDS 1.4 §2.2.3.4 DurabilityQosPolicy Volatile:
125    /// "The Service will not attempt to retain old data beyond what is
126    /// currently held by the DataWriter for live Readers".
127    pub fn skip_samples_up_to(&mut self, sn: SequenceNumber) {
128        if sn > self.highest_sent_sn {
129            self.highest_sent_sn = sn;
130        }
131        if sn > self.highest_acked_sn {
132            self.highest_acked_sn = sn;
133        }
134    }
135
136    /// Updates to the ACKNACK base — the reader has acked all SNs < `base`.
137    /// `base` corresponds to `reader_sn_state.bitmap_base`.
138    pub fn acked_changes_set(&mut self, base: SequenceNumber) {
139        let new_acked = SequenceNumber(base.0 - 1);
140        if new_acked > self.highest_acked_sn {
141            self.highest_acked_sn = new_acked;
142        }
143        // Remove already-acked SNs from requested.
144        self.requested_changes
145            .retain(|sn| *sn > self.highest_acked_sn);
146        // Analogously for fragment-granular requests.
147        self.requested_fragments
148            .retain(|sn, _| *sn > self.highest_acked_sn);
149    }
150
151    /// Remembers the SNs requested in the ACKNACK bitmap for re-send.
152    pub fn requested_changes_set(&mut self, sns: impl IntoIterator<Item = SequenceNumber>) {
153        for sn in sns {
154            if sn > self.highest_acked_sn {
155                self.requested_changes.insert(sn);
156            }
157        }
158    }
159
160    /// Pulls the smallest open requested SN and removes it.
161    pub fn next_requested_change(&mut self) -> Option<SequenceNumber> {
162        let sn = *self.requested_changes.iter().next()?;
163        self.requested_changes.remove(&sn);
164        Some(sn)
165    }
166
167    /// Returns the next not-yet-sent SN, if present in the cache.
168    ///
169    /// `cache_max` is the largest SN currently in the writer cache.
170    pub fn next_unsent_change(&mut self, cache_max: SequenceNumber) -> Option<SequenceNumber> {
171        if self.highest_sent_sn < cache_max {
172            let next = SequenceNumber(self.highest_sent_sn.0 + 1);
173            self.highest_sent_sn = next;
174            Some(next)
175        } else {
176            None
177        }
178    }
179
180    /// True if there are still unacknowledged samples between `highest_acked`
181    /// and `cache_max`.
182    #[must_use]
183    pub fn unacked_changes(&self, cache_max: SequenceNumber) -> bool {
184        cache_max > self.highest_acked_sn
185    }
186
187    /// Getter for `highest_acked_sn`.
188    #[must_use]
189    pub fn highest_acked_sn(&self) -> SequenceNumber {
190        self.highest_acked_sn
191    }
192
193    /// Getter for `highest_sent_sn`.
194    #[must_use]
195    pub fn highest_sent_sn(&self) -> SequenceNumber {
196        self.highest_sent_sn
197    }
198
199    /// Number of queued resend requests.
200    #[must_use]
201    pub fn pending_requested_count(&self) -> usize {
202        self.requested_changes.len()
203    }
204
205    /// Remembers fragment-granular resend requests from a NACK_FRAG.
206    /// SN values ≤ `highest_acked_sn` are ignored.
207    pub fn requested_fragments_set(
208        &mut self,
209        sn: SequenceNumber,
210        fragments: impl IntoIterator<Item = FragmentNumber>,
211    ) {
212        if sn <= self.highest_acked_sn {
213            return;
214        }
215        let entry = self.requested_fragments.entry(sn).or_default();
216        for f in fragments {
217            if f != FragmentNumber::UNKNOWN {
218                entry.insert(f);
219            }
220        }
221        if entry.is_empty() {
222            self.requested_fragments.remove(&sn);
223        }
224    }
225
226    /// Pulls the smallest open (SN, FragmentNumber) pair and removes it.
227    pub fn next_requested_fragment(&mut self) -> Option<(SequenceNumber, FragmentNumber)> {
228        let sn = *self.requested_fragments.keys().next()?;
229        let frag = {
230            let set = self.requested_fragments.get_mut(&sn)?;
231            let f = *set.iter().next()?;
232            set.remove(&f);
233            f
234        };
235        if self
236            .requested_fragments
237            .get(&sn)
238            .is_some_and(alloc::collections::BTreeSet::is_empty)
239        {
240            self.requested_fragments.remove(&sn);
241        }
242        Some((sn, frag))
243    }
244
245    /// Number of queued fragment resends (sum over all SNs).
246    #[must_use]
247    pub fn pending_requested_fragment_count(&self) -> usize {
248        self.requested_fragments.values().map(BTreeSet::len).sum()
249    }
250}
251
252#[cfg(test)]
253#[allow(clippy::expect_used, clippy::unwrap_used)]
254mod tests {
255    use super::*;
256    use crate::wire_types::{EntityId, GuidPrefix};
257
258    fn sn(n: i64) -> SequenceNumber {
259        SequenceNumber(n)
260    }
261
262    fn proxy() -> ReaderProxy {
263        let guid = Guid::new(
264            GuidPrefix::from_bytes([1; 12]),
265            EntityId::user_reader_with_key([0xA0, 0xB0, 0xC0]),
266        );
267        ReaderProxy::new(guid, alloc::vec![], alloc::vec![], true)
268    }
269
270    #[test]
271    fn fresh_proxy_has_zero_state() {
272        let p = proxy();
273        assert_eq!(p.highest_acked_sn(), sn(0));
274        assert_eq!(p.highest_sent_sn(), sn(0));
275        assert_eq!(p.pending_requested_count(), 0);
276    }
277
278    #[test]
279    fn acked_changes_set_monotonic() {
280        let mut p = proxy();
281        p.acked_changes_set(sn(5));
282        assert_eq!(p.highest_acked_sn(), sn(4));
283        // Backwards acks are ignored
284        p.acked_changes_set(sn(3));
285        assert_eq!(p.highest_acked_sn(), sn(4));
286        p.acked_changes_set(sn(10));
287        assert_eq!(p.highest_acked_sn(), sn(9));
288    }
289
290    #[test]
291    fn requested_changes_set_above_ack_only() {
292        let mut p = proxy();
293        p.acked_changes_set(sn(5)); // → highest_acked = 4
294        p.requested_changes_set([sn(2), sn(4), sn(6), sn(8)]);
295        // Only SN > 4 survive
296        assert_eq!(p.pending_requested_count(), 2);
297    }
298
299    #[test]
300    fn next_requested_change_pulls_smallest_first() {
301        let mut p = proxy();
302        p.requested_changes_set([sn(8), sn(3), sn(5)]);
303        assert_eq!(p.next_requested_change(), Some(sn(3)));
304        assert_eq!(p.next_requested_change(), Some(sn(5)));
305        assert_eq!(p.next_requested_change(), Some(sn(8)));
306        assert_eq!(p.next_requested_change(), None);
307    }
308
309    #[test]
310    fn next_unsent_change_walks_sequentially() {
311        let mut p = proxy();
312        let cache_max = sn(3);
313        assert_eq!(p.next_unsent_change(cache_max), Some(sn(1)));
314        assert_eq!(p.next_unsent_change(cache_max), Some(sn(2)));
315        assert_eq!(p.next_unsent_change(cache_max), Some(sn(3)));
316        assert_eq!(p.next_unsent_change(cache_max), None);
317    }
318
319    #[test]
320    fn next_unsent_change_picks_up_after_cache_grows() {
321        let mut p = proxy();
322        assert_eq!(p.next_unsent_change(sn(2)), Some(sn(1)));
323        assert_eq!(p.next_unsent_change(sn(2)), Some(sn(2)));
324        assert_eq!(p.next_unsent_change(sn(2)), None);
325        assert_eq!(p.next_unsent_change(sn(5)), Some(sn(3)));
326    }
327
328    #[test]
329    fn unacked_changes_detects_gap() {
330        let mut p = proxy();
331        assert!(!p.unacked_changes(sn(0)));
332        assert!(p.unacked_changes(sn(5)));
333        p.acked_changes_set(sn(6)); // → highest_acked = 5
334        assert!(!p.unacked_changes(sn(5)));
335        assert!(p.unacked_changes(sn(7)));
336    }
337
338    #[test]
339    fn acking_also_prunes_requested_changes() {
340        let mut p = proxy();
341        p.requested_changes_set([sn(3), sn(5), sn(7)]);
342        assert_eq!(p.pending_requested_count(), 3);
343        p.acked_changes_set(sn(6)); // → highest_acked = 5
344        // sn(3) and sn(5) are now obsolete
345        assert_eq!(p.pending_requested_count(), 1);
346        assert_eq!(p.next_requested_change(), Some(sn(7)));
347    }
348
349    fn frag(n: u32) -> FragmentNumber {
350        FragmentNumber(n)
351    }
352
353    #[test]
354    fn requested_fragments_set_above_ack_only() {
355        let mut p = proxy();
356        p.acked_changes_set(sn(3)); // → highest_acked = 2
357        p.requested_fragments_set(sn(2), [frag(1), frag(2)]);
358        p.requested_fragments_set(sn(5), [frag(1), frag(3)]);
359        assert_eq!(p.pending_requested_fragment_count(), 2);
360    }
361
362    #[test]
363    fn next_requested_fragment_pulls_smallest_sn_first() {
364        let mut p = proxy();
365        p.requested_fragments_set(sn(5), [frag(3), frag(1)]);
366        p.requested_fragments_set(sn(2), [frag(2)]);
367        assert_eq!(p.next_requested_fragment(), Some((sn(2), frag(2))));
368        assert_eq!(p.next_requested_fragment(), Some((sn(5), frag(1))));
369        assert_eq!(p.next_requested_fragment(), Some((sn(5), frag(3))));
370        assert_eq!(p.next_requested_fragment(), None);
371    }
372
373    #[test]
374    fn acking_also_prunes_requested_fragments() {
375        let mut p = proxy();
376        p.requested_fragments_set(sn(3), [frag(1)]);
377        p.requested_fragments_set(sn(7), [frag(2)]);
378        assert_eq!(p.pending_requested_fragment_count(), 2);
379        p.acked_changes_set(sn(5)); // → highest_acked = 4
380        // sn(3) is obsolete
381        assert_eq!(p.pending_requested_fragment_count(), 1);
382        assert_eq!(p.next_requested_fragment(), Some((sn(7), frag(2))));
383    }
384
385    #[test]
386    fn requested_fragments_ignore_unknown_sentinel() {
387        let mut p = proxy();
388        p.requested_fragments_set(sn(1), [FragmentNumber::UNKNOWN, frag(1)]);
389        assert_eq!(p.pending_requested_fragment_count(), 1);
390    }
391
392    // ---- Spec §8.4.15.6 inactive-reader reclaim ----
393
394    #[test]
395    fn proxy_is_inactive_initially_when_threshold_is_short() {
396        // Initial last_activity = ZERO. If the writer checks with
397        // now=10s + threshold=1s, the proxy is inactive.
398        let p = proxy();
399        assert!(p.is_inactive(
400            core::time::Duration::from_secs(10),
401            core::time::Duration::from_secs(1)
402        ));
403    }
404
405    #[test]
406    fn proxy_is_active_after_note_activity() {
407        let mut p = proxy();
408        p.note_activity(core::time::Duration::from_secs(5));
409        assert_eq!(p.last_activity(), core::time::Duration::from_secs(5));
410        // Within the threshold the proxy is active.
411        assert!(!p.is_inactive(
412            core::time::Duration::from_secs(6),
413            core::time::Duration::from_secs(2)
414        ));
415    }
416
417    #[test]
418    fn proxy_becomes_inactive_after_threshold_elapses() {
419        let mut p = proxy();
420        p.note_activity(core::time::Duration::from_secs(5));
421        // 10 seconds later, threshold 2s → inactive.
422        assert!(p.is_inactive(
423            core::time::Duration::from_secs(15),
424            core::time::Duration::from_secs(2)
425        ));
426    }
427
428    #[test]
429    fn proxy_inactivity_not_reported_when_now_before_last_activity() {
430        // Edge case: now < last_activity (clock skew or similar) → no
431        // inactive report.
432        let mut p = proxy();
433        p.note_activity(core::time::Duration::from_secs(100));
434        assert!(!p.is_inactive(
435            core::time::Duration::from_secs(50),
436            core::time::Duration::from_secs(1)
437        ));
438    }
439}