Skip to main content

nros_rmw/
correlation.rs

1//! Request/reply correlation for a service client — issue 2009.
2//!
3//! [`ClientTrait::send_request_raw`] hands back the id the backend assigned to
4//! a request and [`ClientTrait::take_response_raw`] hands back the id of the
5//! request a reply answers (issue 0778). This module is the ONE place that
6//! decides what a client does with the pair, so every surface that takes a
7//! reply — the Rust `Promise`, the executor's callback clients, the action
8//! client's three sub-clients, the C and C++ blocking paths — applies the same
9//! rule:
10//!
11//! * a reply is delivered only to the request whose id it carries;
12//! * a reply whose request is not pending — cancelled, abandoned, timed out,
13//!   or a duplicate of one already answered — is DROPPED, never handed to the
14//!   next request the client sends;
15//! * a backend that cannot supply ids ([`SEQUENCE_ID_UNKNOWN`]) gets a defined
16//!   behaviour instead of a guess: one request in flight per client, and a
17//!   reply without an id is delivered only when exactly one request is
18//!   pending.
19//!
20//! Bounded and allocation-free: a [`RequestCorrelator`] is a fixed table of
21//! `N` `(id, key)` pairs held inline by the client that owns it. A full table
22//! refuses the next send ([`SendRefusal::Full`]) before anything reaches the
23//! wire, so a request is never sent that its reply could not be matched to.
24//!
25//! The C++ `rclcpp::Future` keeps several requests in flight on ONE client
26//! with a reply buffer per future; it applies this same rule over an intrusive
27//! list of its futures (`rclcpp/future.hpp`), because the buffers it delivers
28//! into live in the futures, not in the client.
29//!
30//! [`ClientTrait::send_request_raw`]: crate::ClientTrait::send_request_raw
31//! [`ClientTrait::take_response_raw`]: crate::ClientTrait::take_response_raw
32
33use crate::ClientTrait;
34
35/// The id a backend reports when it cannot supply one — the Rust spelling of
36/// the ABI's `NROS_RMW_SEQUENCE_ID_UNKNOWN` (`INT64_MIN`). The C vtable
37/// adapter pre-sets both out-parameters to it, so a backend that never writes
38/// them reads as "no id" rather than as id 0, which is a legal id.
39pub const SEQUENCE_ID_UNKNOWN: i64 = i64::MIN;
40
41/// How many stale replies one [`take_correlated`] call drops before it gives
42/// up for this poll and reports "nothing yet". Bounds the work a burst of
43/// stray replies can make a single poll do; the rest are dropped by the next
44/// poll.
45pub const MAX_STALE_DRAIN_PER_POLL: usize = 8;
46
47/// Why a send was refused BEFORE it reached the backend.
48#[derive(Clone, Copy, Debug, PartialEq, Eq)]
49pub enum SendRefusal {
50    /// The pending table already holds `N` requests.
51    Full,
52    /// A request the backend sent WITHOUT an id is still pending, so a second
53    /// one could not be told apart from it. Backends that cannot supply ids
54    /// keep one request in flight per client.
55    Unkeyed,
56}
57
58/// A fixed table of the requests a client has in flight, each keyed by the
59/// sequence id the backend assigned and carrying a caller-chosen `K` (`()`
60/// for a plain service client, the `GoalUuid` for an action sub-client).
61///
62/// See the [module docs](self) for the rule it applies.
63#[derive(Clone, Copy, Debug)]
64pub struct RequestCorrelator<K: Copy, const N: usize> {
65    entries: [(i64, K); N],
66    len: u8,
67    /// Requests sent WITHOUT an id that were abandoned (removed or cleared)
68    /// before their reply came. Each costs the next unkeyed reply, which is
69    /// dropped rather than handed to a newer request it cannot be told apart
70    /// from.
71    orphans: u8,
72    /// Replies dropped because no pending request matched them. Wrapping;
73    /// diagnostic only.
74    dropped: u32,
75}
76
77impl<K: Copy + Default, const N: usize> Default for RequestCorrelator<K, N> {
78    fn default() -> Self {
79        Self::new()
80    }
81}
82
83impl<K: Copy + Default, const N: usize> RequestCorrelator<K, N> {
84    /// An empty table.
85    pub fn new() -> Self {
86        const { assert!(N >= 1 && N <= u8::MAX as usize, "N must be 1..=255") };
87        Self {
88            entries: [(SEQUENCE_ID_UNKNOWN, K::default()); N],
89            len: 0,
90            orphans: 0,
91            dropped: 0,
92        }
93    }
94}
95
96impl<K: Copy, const N: usize> RequestCorrelator<K, N> {
97    /// The table's capacity.
98    pub const CAPACITY: usize = N;
99
100    /// Requests currently pending.
101    pub fn len(&self) -> usize {
102        self.len as usize
103    }
104
105    /// No request pending.
106    pub fn is_empty(&self) -> bool {
107        self.len == 0
108    }
109
110    /// Replies dropped so far because no pending request matched them.
111    pub fn dropped(&self) -> u32 {
112        self.dropped
113    }
114
115    fn live(&self) -> &[(i64, K)] {
116        &self.entries[..self.len as usize]
117    }
118
119    /// Whether a new request may be SENT now. Ask this before the send, so a
120    /// refused request never reaches the wire.
121    pub fn can_send(&self) -> Result<(), SendRefusal> {
122        if self.live().iter().any(|(s, _)| *s == SEQUENCE_ID_UNKNOWN) {
123            return Err(SendRefusal::Unkeyed);
124        }
125        if self.len as usize >= N {
126            return Err(SendRefusal::Full);
127        }
128        Ok(())
129    }
130
131    /// Whether `seq` is pending.
132    pub fn contains(&self, seq: i64) -> bool {
133        self.live().iter().any(|(s, _)| *s == seq)
134    }
135
136    /// Record a request the backend accepted. Call after a send
137    /// [`can_send`](Self::can_send) allowed; `Err(Full)` if the table is full
138    /// anyway (nothing is recorded then).
139    pub fn insert(&mut self, seq: i64, key: K) -> Result<(), SendRefusal> {
140        if self.len as usize >= N {
141            return Err(SendRefusal::Full);
142        }
143        self.entries[self.len as usize] = (seq, key);
144        self.len += 1;
145        Ok(())
146    }
147
148    /// Record a request, evicting the OLDEST pending one when the table is
149    /// full. For a RE-SEND of one logical call (`rclc_client_call`'s
150    /// discovery-race retry), where every generation answers the same
151    /// question and the oldest is the least likely to still be answered.
152    pub fn insert_evicting(&mut self, seq: i64, key: K) {
153        if self.len as usize >= N {
154            let evicted = self.entries[0].0;
155            self.remove_at(0);
156            self.note_abandoned(evicted);
157        }
158        let _ = self.insert(seq, key);
159    }
160
161    fn remove_at(&mut self, i: usize) {
162        let len = self.len as usize;
163        self.entries.copy_within(i + 1..len, i);
164        self.len -= 1;
165    }
166
167    fn note_abandoned(&mut self, seq: i64) {
168        if seq == SEQUENCE_ID_UNKNOWN {
169            self.orphans = self.orphans.saturating_add(1);
170        }
171    }
172
173    /// Forget a pending request (cancelled / removed). Its reply, if it still
174    /// comes, is dropped. `true` if it was pending.
175    pub fn remove(&mut self, seq: i64) -> bool {
176        match self.live().iter().position(|(s, _)| *s == seq) {
177            Some(i) => {
178                self.remove_at(i);
179                self.note_abandoned(seq);
180                true
181            }
182            None => false,
183        }
184    }
185
186    /// Forget ONE pending request carrying `key` — the oldest — as
187    /// [`remove`](Self::remove) does by id. For a caller that knows what a
188    /// request was ABOUT but not the id the backend gave it: an action
189    /// client's future is keyed by its goal, so when the last owner of a
190    /// goal's request lets go before the reply, this frees the slot the
191    /// request held (issue 2027). Its reply, if it still comes, is dropped.
192    /// `true` if one was pending.
193    pub fn remove_key(&mut self, key: &K) -> bool
194    where
195        K: PartialEq,
196    {
197        match self.live().iter().position(|(_, k)| k == key) {
198            Some(i) => {
199                let seq = self.entries[i].0;
200                self.remove_at(i);
201                self.note_abandoned(seq);
202                true
203            }
204            None => false,
205        }
206    }
207
208    /// Forget every pending request (the caller abandoned them all — a
209    /// timed-out blocking call, `reset_in_flight`). Their replies are dropped.
210    pub fn clear(&mut self) {
211        for i in 0..self.len as usize {
212            let seq = self.entries[i].0;
213            self.note_abandoned(seq);
214        }
215        self.len = 0;
216    }
217
218    /// Match a reply carrying `reply_seq` to a pending request. `Some(key)`
219    /// removes and returns the request it answers; `None` means the reply is
220    /// STALE and the caller drops it.
221    pub fn resolve(&mut self, reply_seq: i64) -> Option<K> {
222        let matched = if reply_seq == SEQUENCE_ID_UNKNOWN {
223            if self.orphans > 0 {
224                // An abandoned unkeyed request may be the one answering; the
225                // reply cannot say, so it goes to nobody.
226                self.orphans -= 1;
227                None
228            } else if self.len == 1 {
229                Some(0)
230            } else {
231                None
232            }
233        } else {
234            match self.live().iter().position(|(s, _)| *s == reply_seq) {
235                Some(i) => Some(i),
236                // The backend gave the REPLY an id but not the request: the
237                // sole pending request is the only candidate.
238                None if self.len == 1 && self.entries[0].0 == SEQUENCE_ID_UNKNOWN => Some(0),
239                None => None,
240            }
241        };
242        match matched {
243            Some(i) => {
244                let key = self.entries[i].1;
245                self.remove_at(i);
246                Some(key)
247            }
248            None => {
249                self.dropped = self.dropped.wrapping_add(1);
250                None
251            }
252        }
253    }
254}
255
256/// Type-erased [`RequestCorrelator::resolve`], so one `Promise` type can poll
257/// a plain service client and an action sub-client alike.
258pub trait ReplyMatcher {
259    /// `true` if `reply_seq` answers a pending request (which is then retired),
260    /// `false` if the reply is stale and must be dropped.
261    fn match_reply(&mut self, reply_seq: i64) -> bool;
262}
263
264impl<K: Copy, const N: usize> ReplyMatcher for RequestCorrelator<K, N> {
265    fn match_reply(&mut self, reply_seq: i64) -> bool {
266        self.resolve(reply_seq).is_some()
267    }
268}
269
270/// Take the next reply that answers a pending request, dropping stale ones.
271///
272/// `Ok(Some((len, seq, key)))` — a reply for the pending request `key`, its
273/// bytes in `buf[..len]`. `Ok(None)` — nothing for a pending request yet (any
274/// stale replies met on the way were dropped, at most
275/// [`MAX_STALE_DRAIN_PER_POLL`] per call). Errors pass through unchanged.
276pub fn take_correlated<C, K, const N: usize>(
277    client: &mut C,
278    buf: &mut [u8],
279    pending: &mut RequestCorrelator<K, N>,
280) -> Result<Option<(usize, i64, K)>, C::Error>
281where
282    C: ClientTrait + ?Sized,
283    K: Copy,
284{
285    for _ in 0..MAX_STALE_DRAIN_PER_POLL {
286        match client.take_response_raw(buf)? {
287            None => return Ok(None),
288            Some((len, seq)) => {
289                if let Some(key) = pending.resolve(seq) {
290                    return Ok(Some((len, seq, key)));
291                }
292            }
293        }
294    }
295    Ok(None)
296}
297
298/// [`take_correlated`] through the type-erased [`ReplyMatcher`].
299pub fn take_matched<C>(
300    client: &mut C,
301    buf: &mut [u8],
302    pending: &mut dyn ReplyMatcher,
303) -> Result<Option<(usize, i64)>, C::Error>
304where
305    C: ClientTrait + ?Sized,
306{
307    for _ in 0..MAX_STALE_DRAIN_PER_POLL {
308        match client.take_response_raw(buf)? {
309            None => return Ok(None),
310            Some((len, seq)) => {
311                if pending.match_reply(seq) {
312                    return Ok(Some((len, seq)));
313                }
314            }
315        }
316    }
317    Ok(None)
318}
319
320#[cfg(test)]
321mod tests {
322    use super::*;
323    use crate::TransportError;
324    use std::vec::Vec;
325
326    type Table = RequestCorrelator<u32, 4>;
327
328    #[test]
329    fn a_reply_reaches_only_the_request_it_names() {
330        let mut t = Table::new();
331        t.insert(10, 1).unwrap();
332        t.insert(11, 2).unwrap();
333        // Out of order: the second request answers first.
334        assert_eq!(t.resolve(11), Some(2));
335        assert_eq!(t.resolve(10), Some(1));
336        assert!(t.is_empty());
337    }
338
339    #[test]
340    fn remove_key_frees_one_slot_and_drops_its_late_reply() {
341        let mut t = Table::new();
342        for (seq, key) in [(10, 7), (11, 8), (12, 7), (13, 9)] {
343            t.insert(seq, key).unwrap();
344        }
345        assert_eq!(t.can_send(), Err(SendRefusal::Full));
346        assert!(t.remove_key(&7), "the oldest request about 7 is forgotten");
347        assert_eq!(t.len(), 3);
348        assert!(t.can_send().is_ok(), "the freed slot takes a new request");
349        assert_eq!(
350            t.resolve(10),
351            None,
352            "the forgotten request's late reply is dropped"
353        );
354        assert_eq!(
355            t.resolve(12),
356            Some(7),
357            "the other request about 7 is still pending"
358        );
359        assert!(!t.remove_key(&42), "nothing pending about 42");
360        assert_eq!(t.len(), 2);
361    }
362
363    #[test]
364    fn a_reply_for_a_removed_request_is_dropped() {
365        let mut t = Table::new();
366        t.insert(10, 1).unwrap();
367        assert!(t.remove(10));
368        t.insert(11, 2).unwrap();
369        assert_eq!(t.resolve(10), None, "the late reply goes to nobody");
370        assert_eq!(t.dropped(), 1);
371        assert_eq!(
372            t.resolve(11),
373            Some(2),
374            "the next request still gets its own"
375        );
376    }
377
378    #[test]
379    fn a_full_table_refuses_the_send() {
380        let mut t = RequestCorrelator::<(), 2>::new();
381        t.insert(1, ()).unwrap();
382        t.insert(2, ()).unwrap();
383        assert_eq!(t.can_send(), Err(SendRefusal::Full));
384        assert_eq!(t.insert(3, ()), Err(SendRefusal::Full));
385        assert_eq!(t.len(), 2, "a refused insert records nothing");
386        assert_eq!(t.resolve(1), Some(()));
387        assert_eq!(t.can_send(), Ok(()));
388    }
389
390    #[test]
391    fn evicting_insert_retires_the_oldest_generation() {
392        let mut t = RequestCorrelator::<(), 2>::new();
393        t.insert_evicting(1, ());
394        t.insert_evicting(2, ());
395        t.insert_evicting(3, ());
396        assert!(!t.contains(1));
397        assert!(t.contains(2) && t.contains(3));
398    }
399
400    #[test]
401    fn unkeyed_backend_keeps_one_request_in_flight() {
402        let mut t = Table::new();
403        t.insert(SEQUENCE_ID_UNKNOWN, 7).unwrap();
404        assert_eq!(t.can_send(), Err(SendRefusal::Unkeyed));
405        assert_eq!(t.resolve(SEQUENCE_ID_UNKNOWN), Some(7));
406        assert_eq!(t.can_send(), Ok(()));
407    }
408
409    #[test]
410    fn an_unkeyed_reply_is_ambiguous_with_two_pending() {
411        let mut t = Table::new();
412        t.insert(1, 1).unwrap();
413        t.insert(2, 2).unwrap();
414        assert_eq!(t.resolve(SEQUENCE_ID_UNKNOWN), None);
415        assert_eq!(
416            t.len(),
417            2,
418            "neither pending request was given someone else's reply"
419        );
420    }
421
422    #[test]
423    fn an_abandoned_unkeyed_request_costs_the_next_unkeyed_reply() {
424        let mut t = Table::new();
425        t.insert(SEQUENCE_ID_UNKNOWN, 1).unwrap();
426        t.clear();
427        t.insert(SEQUENCE_ID_UNKNOWN, 2).unwrap();
428        assert_eq!(
429            t.resolve(SEQUENCE_ID_UNKNOWN),
430            None,
431            "the reply may be the abandoned request's: dropped, not mismatched"
432        );
433        assert_eq!(t.resolve(SEQUENCE_ID_UNKNOWN), Some(2));
434    }
435
436    /// A client whose replies are scripted, to drive `take_correlated`.
437    struct Scripted(Vec<(u8, i64)>);
438    impl ClientTrait for Scripted {
439        type Error = TransportError;
440        fn send_request_raw(&mut self, _r: &[u8]) -> Result<i64, TransportError> {
441            Ok(0)
442        }
443        fn take_response_raw(
444            &mut self,
445            buf: &mut [u8],
446        ) -> Result<Option<(usize, i64)>, TransportError> {
447            if self.0.is_empty() {
448                return Ok(None);
449            }
450            let (byte, seq) = self.0.remove(0);
451            buf[0] = byte;
452            Ok(Some((1, seq)))
453        }
454    }
455
456    #[test]
457    fn take_correlated_skips_stale_replies_to_reach_a_pending_one() {
458        let mut c = Scripted(std::vec![(0xAA, 99), (0xBB, 5)]);
459        let mut t = Table::new();
460        t.insert(5, 42).unwrap();
461        let mut buf = [0u8; 4];
462        let got = take_correlated(&mut c, &mut buf, &mut t).unwrap();
463        assert_eq!(got, Some((1, 5, 42)));
464        assert_eq!(buf[0], 0xBB);
465        assert_eq!(t.dropped(), 1);
466    }
467
468    #[test]
469    fn take_matched_reports_nothing_when_only_stale_replies_arrive() {
470        let mut c = Scripted(std::vec![(0xAA, 99)]);
471        let mut t = RequestCorrelator::<(), 1>::new();
472        t.insert(5, ()).unwrap();
473        let mut buf = [0u8; 4];
474        assert_eq!(take_matched(&mut c, &mut buf, &mut t).unwrap(), None);
475        assert!(t.contains(5), "the pending request is still waiting");
476    }
477
478    // ---- issue 2033: the typed `ClientTrait` helpers carry the id too ----
479
480    /// A reply of one `i32`, so a test can tell WHICH request a typed reply
481    /// answered by its value.
482    #[derive(Debug, Clone, PartialEq)]
483    struct Sum(i32);
484    impl nros_core::Serialize for Sum {
485        fn serialize(&self, w: &mut nros_core::CdrWriter) -> Result<(), nros_core::SerError> {
486            w.write_i32(self.0)
487        }
488    }
489    impl nros_core::Deserialize for Sum {
490        fn deserialize(r: &mut nros_core::CdrReader) -> Result<Self, nros_core::DeserError> {
491            Ok(Sum(r.read_i32()?))
492        }
493    }
494    impl nros_core::RosMessage for Sum {
495        const TYPE_NAME: &'static str = "test::srv::dds_::Sum_";
496        const TYPE_HASH: &'static str =
497            "0000000000000000000000000000000000000000000000000000000000000000";
498    }
499    struct SumSrv;
500    impl nros_core::RosService for SumSrv {
501        type Request = Sum;
502        type Reply = Sum;
503        const SERVICE_NAME: &'static str = "test::srv::dds_::Sum_";
504        const SERVICE_HASH: &'static str =
505            "0000000000000000000000000000000000000000000000000000000000000000";
506    }
507
508    /// A client that numbers its requests 1, 2, … and answers from a scripted
509    /// queue of `(value, sequence_id)` replies, each as CDR with a header.
510    struct TypedScripted {
511        next_seq: i64,
512        replies: Vec<(i32, i64)>,
513    }
514    impl ClientTrait for TypedScripted {
515        type Error = TransportError;
516        fn send_request_raw(&mut self, _r: &[u8]) -> Result<i64, TransportError> {
517            self.next_seq += 1;
518            Ok(self.next_seq)
519        }
520        fn take_response_raw(
521            &mut self,
522            buf: &mut [u8],
523        ) -> Result<Option<(usize, i64)>, TransportError> {
524            if self.replies.is_empty() {
525                return Ok(None);
526            }
527            let (v, seq) = self.replies.remove(0);
528            let mut w = nros_core::CdrWriter::new_with_header(buf).unwrap();
529            w.write_i32(v).unwrap();
530            Ok(Some((w.position(), seq)))
531        }
532    }
533
534    #[test]
535    fn typed_send_returns_the_id_the_backend_assigned() {
536        let mut c = TypedScripted {
537            next_seq: 40,
538            replies: Vec::new(),
539        };
540        let mut req = [0u8; 16];
541        assert_eq!(c.send_request::<SumSrv>(&Sum(1), &mut req).unwrap(), 41);
542        assert_eq!(c.send_request::<SumSrv>(&Sum(2), &mut req).unwrap(), 42);
543    }
544
545    #[test]
546    fn typed_take_reports_the_id_of_the_request_a_reply_answers() {
547        let mut c = TypedScripted {
548            next_seq: 0,
549            replies: std::vec![(7, 3)],
550        };
551        let mut buf = [0u8; 16];
552        assert_eq!(
553            c.take_response::<SumSrv>(&mut buf).unwrap(),
554            Some((Sum(7), 3))
555        );
556    }
557
558    /// The defect: a typed caller sends, gives up (timeout), resends, and the
559    /// FIRST request's late reply arrives before the second's. The typed take
560    /// for the resend must drop it and deliver only the resend's own reply.
561    #[test]
562    fn typed_take_for_drops_the_late_reply_of_an_abandoned_request() {
563        let mut c = TypedScripted {
564            next_seq: 0,
565            replies: Vec::new(),
566        };
567        let mut req = [0u8; 16];
568        let stale = c.send_request::<SumSrv>(&Sum(1), &mut req).unwrap();
569        // ...no reply in time; the caller resends.
570        let fresh = c.send_request::<SumSrv>(&Sum(2), &mut req).unwrap();
571        assert_ne!(stale, fresh);
572        c.replies.push((100, stale)); // the abandoned request's late reply
573        c.replies.push((200, fresh));
574        let mut buf = [0u8; 16];
575        assert_eq!(
576            c.take_response_for::<SumSrv>(fresh, &mut buf).unwrap(),
577            Some(Sum(200)),
578            "the resend got the abandoned request's reply"
579        );
580        assert!(c.replies.is_empty());
581    }
582
583    #[test]
584    fn typed_take_for_reports_nothing_when_only_a_stale_reply_arrived() {
585        let mut c = TypedScripted {
586            next_seq: 0,
587            replies: std::vec![(100, 1)],
588        };
589        let mut buf = [0u8; 16];
590        assert_eq!(c.take_response_for::<SumSrv>(2, &mut buf).unwrap(), None);
591    }
592
593    #[test]
594    fn typed_take_for_accepts_an_idless_reply_for_the_request_in_mind() {
595        let mut c = TypedScripted {
596            next_seq: 0,
597            replies: std::vec![(5, SEQUENCE_ID_UNKNOWN)],
598        };
599        let mut buf = [0u8; 16];
600        assert_eq!(
601            c.take_response_for::<SumSrv>(9, &mut buf).unwrap(),
602            Some(Sum(5))
603        );
604    }
605}