Skip to main content

zerodds_qos/policies/
reliability.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! ReliabilityQosPolicy (DDS 1.4 §2.2.3.14).
4//!
5//! Wire-Format (DDSI-RTPS §9.6.3.2): u32 kind + Duration max_blocking_time
6//! = 4 + 8 = 12 byte.
7
8use zerodds_cdr::{BufferReader, BufferWriter, DecodeError, EncodeError};
9
10use crate::duration::Duration;
11
12/// Reliability kind (DDS 1.4 §2.2.3.14).
13///
14/// Order per §2.2.3 Table "QoS compatibility":
15/// `BestEffort < Reliable`. `offered.kind >= requested.kind`.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
17#[repr(u32)]
18pub enum ReliabilityKind {
19    /// BestEffort (DDSI-RTPS §8.4.2).
20    BestEffort = 1,
21    /// Reliable (with HB+ACKNACK flow).
22    Reliable = 2,
23}
24
25impl Default for ReliabilityKind {
26    /// DDS 1.4 §2.2.3.14.3: default depends on the entity — reader: BestEffort,
27    /// writer: Reliable. We pick the reader default here; the writer sets
28    /// `ReliabilityKind::Reliable` explicitly.
29    fn default() -> Self {
30        Self::BestEffort
31    }
32}
33
34impl ReliabilityKind {
35    /// Strict mapper.
36    #[must_use]
37    pub const fn try_from_u32(v: u32) -> Option<Self> {
38        match v {
39            1 => Some(Self::BestEffort),
40            2 => Some(Self::Reliable),
41            _ => None,
42        }
43    }
44
45    /// Forward-compatible mapper (unknown → BestEffort).
46    #[must_use]
47    pub const fn from_u32(v: u32) -> Self {
48        match v {
49            2 => Self::Reliable,
50            _ => Self::BestEffort,
51        }
52    }
53}
54
55/// ReliabilityQosPolicy.
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub struct ReliabilityQosPolicy {
58    /// Kind.
59    pub kind: ReliabilityKind,
60    /// Max blocking time — **writer-only semantic**. On the reader side
61    /// the value is serialized (spec wire format) but ignored;
62    /// DDS readers need not interpret the peer value. Cyclone and
63    /// Fast-DDS behave the same — byte-wise identical, semantically
64    /// a reader no-op.
65    pub max_blocking_time: Duration,
66}
67
68impl Default for ReliabilityQosPolicy {
69    fn default() -> Self {
70        Self {
71            kind: ReliabilityKind::BestEffort,
72            // Spec default for a Reliable writer: 100ms.
73            max_blocking_time: Duration::from_millis(100),
74        }
75    }
76}
77
78impl ReliabilityQosPolicy {
79    /// Wire encoding: u32 kind + i32 seconds + u32 fraction = 12 bytes.
80    ///
81    /// # Errors
82    /// Buffer overflow.
83    pub fn encode_into(self, w: &mut BufferWriter) -> Result<(), EncodeError> {
84        w.write_u32(self.kind as u32)?;
85        self.max_blocking_time.encode_into(w)
86    }
87
88    /// Wire decoding (strict). Unknown discriminator → `InvalidEnum`.
89    ///
90    /// # Errors
91    /// Buffer underflow or unknown kind value.
92    pub fn decode_from(r: &mut BufferReader<'_>) -> Result<Self, DecodeError> {
93        let v = r.read_u32()?;
94        let kind = ReliabilityKind::try_from_u32(v).ok_or(DecodeError::InvalidEnum {
95            kind: "ReliabilityKind",
96            value: v,
97        })?;
98        let max_blocking_time = Duration::decode_from(r)?;
99        Ok(Self {
100            kind,
101            max_blocking_time,
102        })
103    }
104}
105
106#[cfg(test)]
107#[allow(clippy::unwrap_used)]
108mod tests {
109    use super::*;
110    use zerodds_cdr::Endianness;
111
112    #[test]
113    fn best_effort_lt_reliable() {
114        assert!(ReliabilityKind::BestEffort < ReliabilityKind::Reliable);
115    }
116
117    #[test]
118    fn try_from_u32_is_strict() {
119        assert_eq!(ReliabilityKind::try_from_u32(0), None);
120        assert_eq!(
121            ReliabilityKind::try_from_u32(1),
122            Some(ReliabilityKind::BestEffort)
123        );
124        assert_eq!(
125            ReliabilityKind::try_from_u32(2),
126            Some(ReliabilityKind::Reliable)
127        );
128        assert_eq!(ReliabilityKind::try_from_u32(3), None);
129    }
130
131    #[test]
132    fn encode_decode_roundtrip() {
133        let p = ReliabilityQosPolicy {
134            kind: ReliabilityKind::Reliable,
135            max_blocking_time: Duration::from_millis(250),
136        };
137        let mut w = BufferWriter::new(Endianness::Little);
138        p.encode_into(&mut w).unwrap();
139        let bytes = w.into_bytes();
140        assert_eq!(bytes.len(), 12);
141        let mut r = BufferReader::new(&bytes, Endianness::Little);
142        assert_eq!(ReliabilityQosPolicy::decode_from(&mut r).unwrap(), p);
143    }
144
145    #[test]
146    fn default_is_best_effort_100ms() {
147        let d = ReliabilityQosPolicy::default();
148        assert_eq!(d.kind, ReliabilityKind::BestEffort);
149        assert_eq!(d.max_blocking_time, Duration::from_millis(100));
150    }
151
152    /// Forward-compatible mapper: unknown -> `BestEffort` (defensive).
153    #[test]
154    fn from_u32_forward_compatible() {
155        assert_eq!(ReliabilityKind::from_u32(1), ReliabilityKind::BestEffort);
156        assert_eq!(ReliabilityKind::from_u32(2), ReliabilityKind::Reliable);
157        // 0 is NOT Best-Effort at the wire level (DDSI-RTPS uses 1/2),
158        // but forward-compat collapses it to BestEffort.
159        assert_eq!(ReliabilityKind::from_u32(0), ReliabilityKind::BestEffort);
160        assert_eq!(ReliabilityKind::from_u32(99), ReliabilityKind::BestEffort);
161    }
162
163    /// Roundtrip of the BestEffort variant incl. non-default
164    /// max_blocking_time (report: variant previously untested).
165    #[test]
166    fn best_effort_roundtrip_with_custom_blocking() {
167        let p = ReliabilityQosPolicy {
168            kind: ReliabilityKind::BestEffort,
169            max_blocking_time: Duration::from_millis(2500),
170        };
171        let mut w = BufferWriter::new(Endianness::Little);
172        p.encode_into(&mut w).unwrap();
173        let bytes = w.into_bytes();
174        assert_eq!(bytes.len(), 12);
175        // Wire-format spec check: first u32 is kind=1 (little-endian).
176        assert_eq!(&bytes[0..4], &[1u8, 0, 0, 0]);
177        let mut r = BufferReader::new(&bytes, Endianness::Little);
178        assert_eq!(ReliabilityQosPolicy::decode_from(&mut r).unwrap(), p);
179    }
180
181    /// Debug format contains both variants and the duration.
182    #[test]
183    fn debug_and_clone_work() {
184        let p = ReliabilityQosPolicy {
185            kind: ReliabilityKind::Reliable,
186            max_blocking_time: Duration::from_secs(1),
187        };
188        #[allow(clippy::clone_on_copy)]
189        let p2 = p.clone();
190        assert_eq!(p, p2);
191        let dbg = alloc::format!("{p:?}");
192        assert!(dbg.contains("Reliable"), "debug: {dbg}");
193    }
194
195    /// Decode with an unknown kind discriminator → `InvalidEnum` error.
196    #[test]
197    fn decode_unknown_kind_errors() {
198        let mut w = BufferWriter::new(Endianness::Little);
199        w.write_u32(5).unwrap();
200        Duration::ZERO.encode_into(&mut w).unwrap();
201        let bytes = w.into_bytes();
202        let mut r = BufferReader::new(&bytes, Endianness::Little);
203        let err = ReliabilityQosPolicy::decode_from(&mut r).unwrap_err();
204        assert!(matches!(
205            err,
206            zerodds_cdr::DecodeError::InvalidEnum {
207                kind: "ReliabilityKind",
208                value: 5
209            }
210        ));
211    }
212
213    /// Decode with a short buffer (kind=Reliable, no blocking-time) →
214    /// short-read error on the duration decode.
215    #[test]
216    fn decode_short_buffer_no_duration_errors() {
217        let mut w = BufferWriter::new(Endianness::Little);
218        w.write_u32(2).unwrap(); // kind=Reliable
219        let bytes = w.into_bytes();
220        let mut r = BufferReader::new(&bytes, Endianness::Little);
221        assert!(ReliabilityQosPolicy::decode_from(&mut r).is_err());
222    }
223
224    /// Spec ordering: BestEffort < Reliable → `offered >= requested`
225    /// compatibility logic builds on this.
226    #[test]
227    fn ordering_reliable_higher_than_besteffort() {
228        assert!(ReliabilityKind::Reliable > ReliabilityKind::BestEffort);
229    }
230}