1use core::time::Duration;
34
35extern crate alloc;
36use alloc::vec::Vec;
37
38use alloc::rc::Rc;
39
40use crate::error::WireError;
41use crate::header::RtpsHeader;
42use crate::history_cache::{CacheChange, ChangeKind, HistoryCache, HistoryKind};
43use crate::message_builder::{AddError, MessageBuilder, OutboundDatagram};
44use crate::reader_proxy::ReaderProxy;
45use crate::submessage_header::{FLAG_E_LITTLE_ENDIAN, SubmessageId};
46use crate::submessages::{
47 DATA_FLAG_DATA, DataFragSubmessage, DataSubmessage, GapSubmessage, HeartbeatSubmessage,
48 NackFragSubmessage, SequenceNumberSet,
49};
50use crate::wire_types::{EntityId, FragmentNumber, Guid, Locator, SequenceNumber, VendorId};
51
52pub const DEFAULT_HEARTBEAT_PERIOD: Duration = Duration::from_millis(100);
65
66pub const DEFAULT_FRAGMENT_SIZE: u32 = 1344;
69
70pub const LOOPBACK_FRAGMENT_SIZE: u32 = 63_000;
76
77pub const LOOPBACK_MTU: usize = 64_000;
80
81#[derive(Debug, Clone)]
83pub struct ReliableWriter {
84 guid: Guid,
85 vendor_id: VendorId,
86 reader_proxies: Vec<ReaderProxy>,
87 cache: HistoryCache,
88 heartbeat_period: Duration,
89 last_heartbeat: Option<Duration>,
90 heartbeat_count: i32,
91 nackfrag_count: i32,
92 unknown_src_count: u64,
96 next_sn: i64,
97 fragment_size: u32,
98 mtu: usize,
99 emit_info_dst: bool,
104 payload_pool: alloc::vec::Vec<alloc::sync::Arc<[u8]>>,
114 payload_pool_cap: usize,
117}
118
119#[derive(Debug, Clone)]
121pub struct ReliableWriterConfig {
122 pub guid: Guid,
124 pub vendor_id: VendorId,
126 pub reader_proxies: Vec<ReaderProxy>,
128 pub max_samples: usize,
131 pub history_kind: HistoryKind,
137 pub heartbeat_period: Duration,
139 pub fragment_size: u32,
141 pub mtu: usize,
143}
144
145impl ReliableWriter {
146 #[must_use]
152 pub fn new(cfg: ReliableWriterConfig) -> Self {
153 assert!(cfg.fragment_size > 0, "fragment_size must be > 0");
154 assert!(cfg.mtu >= 20, "mtu must accommodate RTPS header");
155 Self {
156 guid: cfg.guid,
157 vendor_id: cfg.vendor_id,
158 reader_proxies: cfg.reader_proxies,
159 cache: HistoryCache::new_with_kind(cfg.history_kind, cfg.max_samples),
160 heartbeat_period: cfg.heartbeat_period,
161 last_heartbeat: None,
162 heartbeat_count: 0,
163 nackfrag_count: 0,
164 unknown_src_count: 0,
165 next_sn: 0,
166 fragment_size: cfg.fragment_size,
167 mtu: cfg.mtu,
168 emit_info_dst: false,
169 payload_pool: alloc::vec::Vec::with_capacity(8),
170 payload_pool_cap: 8,
171 }
172 }
173
174 #[must_use]
176 pub fn guid(&self) -> Guid {
177 self.guid
178 }
179
180 pub fn set_emit_info_dst(&mut self, v: bool) {
184 self.emit_info_dst = v;
185 }
186
187 fn open_builder(&self, idx: usize, targets: &Rc<Vec<Locator>>) -> MessageBuilder {
190 let mut b = MessageBuilder::open(self.rtps_header(), Rc::clone(targets), self.mtu);
191 if self.emit_info_dst {
192 let prefix = self.reader_proxies[idx].remote_reader_guid.prefix;
193 let _ = b.try_add_submessage(SubmessageId::InfoDst, 0, &prefix.to_bytes());
194 }
195 b
196 }
197
198 #[must_use]
200 pub fn reader_proxies(&self) -> &[ReaderProxy] {
201 &self.reader_proxies
202 }
203
204 #[must_use]
206 pub fn reader_proxy_count(&self) -> usize {
207 self.reader_proxies.len()
208 }
209
210 pub fn remove_samples_up_to(&mut self, up_to_exclusive: SequenceNumber) -> usize {
215 self.cache.remove_up_to(up_to_exclusive)
216 }
217
218 #[must_use]
220 pub fn cache(&self) -> &HistoryCache {
221 &self.cache
222 }
223
224 pub fn set_cache_kind_and_max(
229 &mut self,
230 kind: crate::history_cache::HistoryKind,
231 max_samples: usize,
232 ) {
233 self.cache.set_kind_and_max(kind, max_samples);
234 }
235
236 #[must_use]
238 pub fn heartbeat_count(&self) -> i32 {
239 self.heartbeat_count
240 }
241
242 #[must_use]
244 pub fn nackfrag_count(&self) -> i32 {
245 self.nackfrag_count
246 }
247
248 #[must_use]
252 pub fn unknown_src_count(&self) -> u64 {
253 self.unknown_src_count
254 }
255
256 #[must_use]
258 pub fn fragment_size(&self) -> u32 {
259 self.fragment_size
260 }
261
262 pub fn set_fragmentation(&mut self, fragment_size: u32, mtu: usize) {
273 assert!(fragment_size > 0, "fragment_size must be > 0");
274 assert!(mtu >= 20, "mtu must accommodate RTPS header");
275 self.fragment_size = fragment_size;
276 self.mtu = mtu;
277 }
278
279 #[must_use]
282 fn needs_fragmentation(&self, payload: &[u8]) -> bool {
283 u32::try_from(payload.len()).unwrap_or(u32::MAX) > self.fragment_size && !payload.is_empty()
284 }
285
286 #[must_use]
294 pub fn all_samples_acknowledged(&self) -> bool {
295 let Some(max_sn) = self.cache.max_sn() else {
296 return true;
297 };
298 self.reader_proxies
299 .iter()
300 .all(|p| p.highest_acked_sn() >= max_sn)
301 }
302
303 pub fn add_reader_proxy(&mut self, proxy: ReaderProxy) {
315 let guid = proxy.remote_reader_guid;
316 if let Some(idx) = self
317 .reader_proxies
318 .iter()
319 .position(|p| p.remote_reader_guid == guid)
320 {
321 self.reader_proxies[idx] = proxy;
322 } else {
323 self.reader_proxies.push(proxy);
324 }
325 self.last_heartbeat = None;
328 }
329
330 pub fn remove_reader_proxy(&mut self, guid: Guid) -> Option<ReaderProxy> {
332 let idx = self
333 .reader_proxies
334 .iter()
335 .position(|p| p.remote_reader_guid == guid)?;
336 Some(self.reader_proxies.remove(idx))
337 }
338
339 pub fn write(&mut self, payload: &[u8]) -> Result<Vec<OutboundDatagram>, WireError> {
350 self.write_stamped(payload, None)
351 }
352
353 pub fn write_stamped(
361 &mut self,
362 payload: &[u8],
363 source_timestamp: Option<crate::header_extension::HeTimestamp>,
364 ) -> Result<Vec<OutboundDatagram>, WireError> {
365 let (sn, payload) = self.stage_sample(payload, source_timestamp)?;
366
367 let mut out = Vec::new();
368 for idx in 0..self.reader_proxies.len() {
369 let advanced = self.reader_proxies[idx].next_unsent_change(sn);
384 if advanced != Some(sn) {
385 continue;
386 }
387 let reader_id = self.reader_proxies[idx].remote_reader_guid.entity_id;
388 let targets = self.targets_for(idx);
389 out.extend(self.build_sample_datagrams(sn, &payload, reader_id, &targets)?);
397 }
398 Ok(out)
399 }
400
401 pub fn write_with_heartbeat(
417 &mut self,
418 payload: &[u8],
419 now: Duration,
420 ) -> Result<Vec<OutboundDatagram>, WireError> {
421 self.write_with_heartbeat_stamped(payload, now, None)
422 }
423
424 pub fn write_with_heartbeat_stamped(
430 &mut self,
431 payload: &[u8],
432 now: Duration,
433 source_timestamp: Option<crate::header_extension::HeTimestamp>,
434 ) -> Result<Vec<OutboundDatagram>, WireError> {
435 let (sn, payload) = self.stage_sample(payload, source_timestamp)?;
436 let cache_min = self.cache.min_sn().unwrap_or(SequenceNumber(1));
439 let fragmented = self.needs_fragmentation(&payload);
440 let mut out = Vec::new();
441 for idx in 0..self.reader_proxies.len() {
442 let advanced = self.reader_proxies[idx].next_unsent_change(sn) == Some(sn);
443 let reader_id = self.reader_proxies[idx].remote_reader_guid.entity_id;
444 let targets = self.targets_for(idx);
445 let hb_first_sn = cache_min.max(SequenceNumber(
455 self.reader_proxies[idx]
456 .highest_acked_sn()
457 .0
458 .saturating_add(1),
459 ));
460 if advanced && !fragmented {
461 let mut builder = self.open_builder(idx, &targets);
469 self.append_data(&mut builder, sn, &payload, reader_id, &mut out, &targets)?;
470 let coframe = builder.len() % 4 == 0;
486 if coframe {
487 self.append_heartbeat(
488 &mut builder,
489 reader_id,
490 hb_first_sn,
491 false,
492 &mut out,
493 &targets,
494 )?;
495 }
496 if let Some(dg) = builder.finish() {
497 out.push(dg);
498 }
499 if !coframe {
500 let mut hb_builder = self.open_builder(idx, &targets);
501 self.append_heartbeat(
502 &mut hb_builder,
503 reader_id,
504 hb_first_sn,
505 false,
506 &mut out,
507 &targets,
508 )?;
509 if let Some(dg) = hb_builder.finish() {
510 out.push(dg);
511 }
512 }
513 } else {
514 if advanced {
518 out.extend(self.build_sample_datagrams(sn, &payload, reader_id, &targets)?);
519 }
520 let mut builder = self.open_builder(idx, &targets);
521 self.append_heartbeat(
522 &mut builder,
523 reader_id,
524 hb_first_sn,
525 false,
526 &mut out,
527 &targets,
528 )?;
529 if let Some(dg) = builder.finish() {
530 out.push(dg);
531 }
532 }
533 }
534 self.last_heartbeat = Some(now);
535 Ok(out)
536 }
537
538 fn stage_sample(
548 &mut self,
549 payload: &[u8],
550 source_timestamp: Option<crate::header_extension::HeTimestamp>,
551 ) -> Result<(SequenceNumber, alloc::sync::Arc<[u8]>), WireError> {
552 let sn_value = self
553 .next_sn
554 .checked_add(1)
555 .ok_or(WireError::ValueOutOfRange {
556 message: "sequence number overflow",
557 })?;
558 self.next_sn = sn_value;
559 let sn = SequenceNumber(sn_value);
560
561 let arc_payload = loop {
565 match self.payload_pool.pop() {
566 None => {
567 break alloc::sync::Arc::<[u8]>::from(payload);
569 }
570 Some(mut arc) => {
571 let len_match = arc.len() == payload.len();
572 let exclusive = alloc::sync::Arc::strong_count(&arc) == 1
573 && alloc::sync::Arc::weak_count(&arc) == 0;
574 if len_match && exclusive {
575 if let Some(slice) = alloc::sync::Arc::get_mut(&mut arc) {
578 slice.copy_from_slice(payload);
579 break arc;
580 }
581 }
584 drop(arc);
587 }
588 }
589 };
590
591 let evicted = self
593 .cache
594 .insert_returning_evicted(
595 CacheChange::alive_arc(sn, alloc::sync::Arc::clone(&arc_payload))
596 .with_source_timestamp(source_timestamp),
597 )
598 .map_err(|_| WireError::ValueOutOfRange {
599 message: "history cache full or duplicate",
600 })?;
601 if let Some(evicted_change) = evicted {
602 if self.payload_pool.len() < self.payload_pool_cap {
603 self.payload_pool.push(evicted_change.payload);
604 }
605 }
607 Ok((sn, arc_payload))
608 }
609
610 pub fn tick(&mut self, now: Duration) -> Result<Vec<OutboundDatagram>, WireError> {
617 let should_heartbeat = match self.last_heartbeat {
618 None => true,
619 Some(last) => now.saturating_sub(last) >= self.heartbeat_period,
620 };
621 let emit_hb = should_heartbeat && !self.cache.is_empty();
622
623 let mut out = Vec::new();
624 let mut hb_emitted_any = false;
625
626 for idx in 0..self.reader_proxies.len() {
627 let reader_id = self.reader_proxies[idx].remote_reader_guid.entity_id;
628 let targets = self.targets_for(idx);
629
630 while let Some((sn, frag)) = self.reader_proxies[idx].next_requested_fragment() {
632 match self.cache.get(sn) {
633 Some(change) => {
634 let payload = change.payload.clone();
635 #[cfg(feature = "metrics")]
636 crate::metrics::inc_retransmit();
637 #[cfg(feature = "metrics")]
638 crate::metrics::inc_fragmented_sample();
639 out.push(
640 self.build_data_frag_datagram(sn, frag, &payload, reader_id, &targets)?,
641 );
642 }
643 None => {
644 out.push(self.build_gap_datagram(sn, reader_id, &targets)?);
645 }
646 }
647 }
648
649 let mut builder = self.open_builder(idx, &targets);
651
652 while let Some(sn) = self.reader_proxies[idx].next_requested_change() {
653 #[cfg(feature = "metrics")]
654 crate::metrics::inc_retransmit();
655 match self.cache.get(sn) {
656 Some(change) => {
657 let payload = change.payload.clone();
658 if self.needs_fragmentation(&payload) {
660 if let Some(dg) = builder.finish() {
661 out.push(dg);
662 }
663 builder = MessageBuilder::open(
664 self.rtps_header(),
665 Rc::clone(&targets),
666 self.mtu,
667 );
668 out.extend(
669 self.build_sample_datagrams(sn, &payload, reader_id, &targets)?,
670 );
671 } else {
672 self.append_data(
673 &mut builder,
674 sn,
675 &payload,
676 reader_id,
677 &mut out,
678 &targets,
679 )?;
680 }
681 }
682 None => {
683 self.append_gap(&mut builder, sn, reader_id, &mut out, &targets)?;
684 }
685 }
686 }
687
688 if emit_hb {
706 let cache_min = self.cache.min_sn().unwrap_or(SequenceNumber(1));
707 let per_proxy_first = SequenceNumber(
708 self.reader_proxies[idx]
709 .highest_acked_sn()
710 .0
711 .saturating_add(1),
712 );
713 let first_sn = cache_min.max(per_proxy_first);
714 self.append_heartbeat(
715 &mut builder,
716 reader_id,
717 first_sn,
718 false,
719 &mut out,
720 &targets,
721 )?;
722 hb_emitted_any = true;
723 }
724
725 if let Some(dg) = builder.finish() {
726 out.push(dg);
727 }
728 }
729
730 if hb_emitted_any {
731 self.last_heartbeat = Some(now);
732 }
733
734 Ok(out)
735 }
736
737 pub fn handle_acknack(
742 &mut self,
743 src_guid: Guid,
744 base: SequenceNumber,
745 requested: impl IntoIterator<Item = SequenceNumber>,
746 ) {
747 #[cfg(feature = "metrics")]
748 crate::metrics::inc_acknack_received();
749 let Some(idx) = self
750 .reader_proxies
751 .iter()
752 .position(|p| p.remote_reader_guid == src_guid)
753 else {
754 self.unknown_src_count = self.unknown_src_count.saturating_add(1);
755 return;
756 };
757 let requested: alloc::vec::Vec<SequenceNumber> = requested.into_iter().collect();
758 let bitmap_empty = requested.is_empty();
759 self.reader_proxies[idx].acked_changes_set(base);
760 self.reader_proxies[idx].requested_changes_set(requested);
761 if bitmap_empty {
770 if let Some(max_sn) = self.cache.max_sn() {
771 let proxy = &mut self.reader_proxies[idx];
772 if proxy.unacked_changes(max_sn) {
773 let from = proxy.highest_acked_sn().0 + 1;
774 proxy.requested_changes_set((from..=max_sn.0).map(SequenceNumber));
775 }
776 }
777 }
778 }
783
784 pub fn handle_nackfrag(&mut self, src_guid: Guid, nf: &NackFragSubmessage) {
786 if nf.writer_id != self.guid.entity_id {
787 return;
788 }
789 let Some(idx) = self
790 .reader_proxies
791 .iter()
792 .position(|p| p.remote_reader_guid == src_guid)
793 else {
794 self.unknown_src_count = self.unknown_src_count.saturating_add(1);
795 return;
796 };
797 self.nackfrag_count = self.nackfrag_count.wrapping_add(1);
798 let missing: Vec<FragmentNumber> = nf.fragment_number_state.iter_set().collect();
799 self.reader_proxies[idx].requested_fragments_set(nf.writer_sn, missing);
800 }
801
802 fn rtps_header(&self) -> RtpsHeader {
805 RtpsHeader::new(self.vendor_id, self.guid.prefix)
806 }
807
808 fn targets_for(&self, idx: usize) -> Rc<Vec<Locator>> {
811 let p = &self.reader_proxies[idx];
812 if !p.multicast_locators.is_empty() {
813 Rc::new(p.multicast_locators.clone())
814 } else {
815 Rc::new(p.unicast_locators.clone())
816 }
817 }
818
819 fn append_info_ts(&self, builder: &mut MessageBuilder, sn: SequenceNumber) {
826 if let Some(ts) = self.cache.get(sn).and_then(|c| c.source_timestamp) {
827 let its = crate::submessages::InfoTimestampSubmessage {
828 timestamp: ts,
829 invalidate: false,
830 };
831 let (body, flags) = its.write_body(true);
832 let _ = builder.try_add_submessage(SubmessageId::InfoTs, flags, &body);
833 }
834 }
835
836 fn append_data(
837 &self,
838 builder: &mut MessageBuilder,
839 sn: SequenceNumber,
840 payload: &alloc::sync::Arc<[u8]>,
841 reader_id: EntityId,
842 out: &mut Vec<OutboundDatagram>,
843 targets: &Rc<Vec<Locator>>,
844 ) -> Result<(), WireError> {
845 self.append_info_ts(builder, sn);
846 let mut header = [0u8; 20];
857 header[0] = 0;
859 header[1] = 0;
860 header[2] = 16;
862 header[3] = 0;
863 header[4..8].copy_from_slice(&reader_id.to_bytes());
865 header[8..12].copy_from_slice(&self.guid.entity_id.to_bytes());
867 header[12..20].copy_from_slice(&sn.to_bytes_le());
869 let flags = FLAG_E_LITTLE_ENDIAN | DATA_FLAG_DATA;
870 match builder.try_add_submessage_split(SubmessageId::Data, flags, &header, payload) {
874 Ok(()) => Ok(()),
875 Err(AddError::BodyTooLarge) => Err(WireError::ValueOutOfRange {
876 message: "DATA body exceeds u16::MAX",
877 }),
878 Err(AddError::WouldExceedMtu { .. }) => {
879 let finished = core::mem::replace(
880 builder,
881 MessageBuilder::open(self.rtps_header(), Rc::clone(targets), self.mtu),
882 );
883 if let Some(dg) = finished.finish() {
884 out.push(dg);
885 }
886 builder
887 .try_add_submessage_split(SubmessageId::Data, flags, &header, payload)
888 .map_err(|_| WireError::ValueOutOfRange {
889 message: "submessage does not fit into fresh datagram",
890 })
891 }
892 }
893 }
894
895 fn append_gap(
896 &self,
897 builder: &mut MessageBuilder,
898 sn: SequenceNumber,
899 reader_id: EntityId,
900 out: &mut Vec<OutboundDatagram>,
901 targets: &Rc<Vec<Locator>>,
902 ) -> Result<(), WireError> {
903 let gap = GapSubmessage {
904 reader_id,
905 writer_id: self.guid.entity_id,
906 gap_start: sn,
907 gap_list: SequenceNumberSet {
908 bitmap_base: SequenceNumber(sn.0 + 1),
909 num_bits: 0,
910 bitmap: Vec::new(),
911 },
912 group_info: None,
913 filtered_count: None,
914 };
915 let (body, flags) = gap.write_body(true);
916 self.append_submessage(
917 builder,
918 SubmessageId::Gap,
919 flags,
920 &body,
921 out,
922 targets,
923 "GAP",
924 )
925 }
926
927 fn append_heartbeat(
928 &mut self,
929 builder: &mut MessageBuilder,
930 reader_id: EntityId,
931 first_sn: SequenceNumber,
932 final_flag: bool,
933 out: &mut Vec<OutboundDatagram>,
934 targets: &Rc<Vec<Locator>>,
935 ) -> Result<(), WireError> {
936 #[cfg(feature = "metrics")]
937 crate::metrics::inc_heartbeat_sent();
938 self.heartbeat_count = self.heartbeat_count.wrapping_add(1);
939 let last = self.cache.max_sn().unwrap_or(SequenceNumber(0));
940 let hb = HeartbeatSubmessage {
941 reader_id,
942 writer_id: self.guid.entity_id,
943 first_sn,
944 last_sn: last,
945 count: self.heartbeat_count,
946 final_flag,
947 liveliness_flag: false,
948 group_info: None,
949 };
950 let (body, flags) = hb.write_body(true);
951 self.append_submessage(
952 builder,
953 SubmessageId::Heartbeat,
954 flags,
955 &body,
956 out,
957 targets,
958 "HEARTBEAT",
959 )
960 }
961
962 #[allow(clippy::too_many_arguments)]
964 fn append_submessage(
965 &self,
966 builder: &mut MessageBuilder,
967 id: SubmessageId,
968 flags: u8,
969 body: &[u8],
970 out: &mut Vec<OutboundDatagram>,
971 targets: &Rc<Vec<Locator>>,
972 kind_hint: &'static str,
973 ) -> Result<(), WireError> {
974 match builder.try_add_submessage(id, flags, body) {
975 Ok(()) => Ok(()),
976 Err(AddError::BodyTooLarge) => Err(WireError::ValueOutOfRange {
977 message: match kind_hint {
978 "DATA" => "DATA body exceeds u16::MAX",
979 "GAP" => "GAP body exceeds u16::MAX",
980 "HEARTBEAT" => "HEARTBEAT body exceeds u16::MAX",
981 _ => "submessage body exceeds u16::MAX",
982 },
983 }),
984 Err(AddError::WouldExceedMtu { .. }) => {
985 let finished = core::mem::replace(
986 builder,
987 MessageBuilder::open(self.rtps_header(), Rc::clone(targets), self.mtu),
988 );
989 if let Some(dg) = finished.finish() {
990 out.push(dg);
991 }
992 builder.try_add_submessage(id, flags, body).map_err(|_| {
993 WireError::ValueOutOfRange {
994 message: "submessage does not fit into fresh datagram",
995 }
996 })
997 }
998 }
999 }
1000
1001 fn build_sample_datagrams(
1004 &self,
1005 sn: SequenceNumber,
1006 payload: &alloc::sync::Arc<[u8]>,
1007 reader_id: EntityId,
1008 targets: &Rc<Vec<Locator>>,
1009 ) -> Result<Vec<OutboundDatagram>, WireError> {
1010 if !self.needs_fragmentation(payload) {
1011 return Ok(alloc::vec![
1012 self.build_single_data_datagram(sn, payload, reader_id, targets,)?
1013 ]);
1014 }
1015 let frag_size = self.fragment_size as usize;
1016 let sample_size = u32::try_from(payload.len()).map_err(|_| WireError::ValueOutOfRange {
1017 message: "sample size exceeds u32::MAX",
1018 })?;
1019 let frag_size_u16 = u16::try_from(frag_size).map_err(|_| WireError::ValueOutOfRange {
1020 message: "fragment_size exceeds u16::MAX",
1021 })?;
1022 let mut out = Vec::new();
1023 let mut frag_num: u32 = 1;
1024 let mut pos = 0usize;
1025 while pos < payload.len() {
1026 let end = core::cmp::min(pos + frag_size, payload.len());
1027 out.push(self.build_data_frag_submessage_datagram(
1028 sn,
1029 FragmentNumber(frag_num),
1030 frag_size_u16,
1031 sample_size,
1032 &payload[pos..end],
1033 reader_id,
1034 targets,
1035 )?);
1036 pos = end;
1037 frag_num = frag_num.checked_add(1).ok_or(WireError::ValueOutOfRange {
1038 message: "fragment number overflow",
1039 })?;
1040 }
1041 Ok(out)
1042 }
1043
1044 fn build_single_data_datagram(
1045 &self,
1046 sn: SequenceNumber,
1047 payload: &alloc::sync::Arc<[u8]>,
1048 reader_id: EntityId,
1049 targets: &Rc<Vec<Locator>>,
1050 ) -> Result<OutboundDatagram, WireError> {
1051 let mut builder = MessageBuilder::open(self.rtps_header(), Rc::clone(targets), self.mtu);
1052 self.append_info_ts(&mut builder, sn);
1053 let data = DataSubmessage {
1054 extra_flags: 0,
1055 reader_id,
1056 writer_id: self.guid.entity_id,
1057 writer_sn: sn,
1058 inline_qos: None,
1060 key_flag: false,
1061 non_standard_flag: false,
1062 serialized_payload: alloc::sync::Arc::clone(payload),
1063 };
1064 let (body, flags) = data.write_body(true);
1065 builder
1066 .try_add_submessage(SubmessageId::Data, flags, &body)
1067 .map_err(|_| WireError::ValueOutOfRange {
1068 message: "DATA submessage does not fit into MTU",
1069 })?;
1070 builder.finish().ok_or(WireError::ValueOutOfRange {
1071 message: "MessageBuilder finish returned no datagram",
1072 })
1073 }
1074
1075 fn build_lifecycle_datagram(
1081 &self,
1082 sn: SequenceNumber,
1083 key_hash: [u8; 16],
1084 status_bits: u32,
1085 reader_id: EntityId,
1086 targets: &Rc<Vec<Locator>>,
1087 ) -> Result<OutboundDatagram, WireError> {
1088 let mut builder = MessageBuilder::open(self.rtps_header(), Rc::clone(targets), self.mtu);
1089 let inline_qos = crate::inline_qos::lifecycle_inline_qos(key_hash, status_bits);
1090 let data = DataSubmessage {
1091 extra_flags: 0,
1092 reader_id,
1093 writer_id: self.guid.entity_id,
1094 writer_sn: sn,
1095 inline_qos: Some(inline_qos),
1096 key_flag: true,
1097 non_standard_flag: false,
1098 serialized_payload: alloc::sync::Arc::from(alloc::vec::Vec::new()),
1099 };
1100 let (body, flags) = data.write_body(true);
1101 builder
1102 .try_add_submessage(SubmessageId::Data, flags, &body)
1103 .map_err(|_| WireError::ValueOutOfRange {
1104 message: "lifecycle DATA submessage does not fit into MTU",
1105 })?;
1106 builder.finish().ok_or(WireError::ValueOutOfRange {
1107 message: "MessageBuilder finish returned no datagram",
1108 })
1109 }
1110
1111 pub fn write_lifecycle(
1125 &mut self,
1126 key_hash: [u8; 16],
1127 status_bits: u32,
1128 ) -> Result<Vec<OutboundDatagram>, WireError> {
1129 let sn_value = self
1130 .next_sn
1131 .checked_add(1)
1132 .ok_or(WireError::ValueOutOfRange {
1133 message: "sequence number overflow",
1134 })?;
1135 self.next_sn = sn_value;
1136 let sn = SequenceNumber(sn_value);
1137
1138 let kind = match (
1139 status_bits & crate::inline_qos::status_info::DISPOSED != 0,
1140 status_bits & crate::inline_qos::status_info::UNREGISTERED != 0,
1141 ) {
1142 (true, true) => crate::history_cache::ChangeKind::NotAliveDisposedUnregistered,
1143 (true, false) => crate::history_cache::ChangeKind::NotAliveDisposed,
1144 (false, true) => crate::history_cache::ChangeKind::NotAliveUnregistered,
1145 (false, false) => {
1146 return Err(WireError::ValueOutOfRange {
1147 message: "lifecycle send requires DISPOSED or UNREGISTERED bit",
1148 });
1149 }
1150 };
1151
1152 self.cache
1155 .insert(crate::history_cache::CacheChange::lifecycle(
1156 sn,
1157 key_hash.to_vec(),
1158 kind,
1159 ))
1160 .map_err(|_| WireError::ValueOutOfRange {
1161 message: "history cache full or duplicate (lifecycle)",
1162 })?;
1163
1164 let mut out = Vec::new();
1165 for idx in 0..self.reader_proxies.len() {
1166 let reader_id = self.reader_proxies[idx].remote_reader_guid.entity_id;
1167 let targets = self.targets_for(idx);
1168 loop {
1183 let Some(next) = self.reader_proxies[idx].next_unsent_change(sn) else {
1184 break;
1185 };
1186 match self.cache.get(next) {
1187 Some(change) => match change.kind {
1188 ChangeKind::Alive | ChangeKind::AliveFiltered => {
1189 let payload = change.payload.clone();
1190 out.extend(
1191 self.build_sample_datagrams(next, &payload, reader_id, &targets)?,
1192 );
1193 }
1194 marker_kind => {
1195 let kh = lifecycle_key_hash(&change.payload).unwrap_or(key_hash);
1198 let bits = status_bits_for_kind(marker_kind);
1199 out.push(
1200 self.build_lifecycle_datagram(next, kh, bits, reader_id, &targets)?,
1201 );
1202 }
1203 },
1204 None => {
1205 out.push(self.build_gap_datagram(next, reader_id, &targets)?);
1206 }
1207 }
1208 if next == sn {
1209 break;
1210 }
1211 }
1212 }
1213 Ok(out)
1214 }
1215
1216 fn build_data_frag_datagram(
1217 &self,
1218 sn: SequenceNumber,
1219 frag: FragmentNumber,
1220 full_payload: &alloc::sync::Arc<[u8]>,
1221 reader_id: EntityId,
1222 targets: &Rc<Vec<Locator>>,
1223 ) -> Result<OutboundDatagram, WireError> {
1224 let frag_size = self.fragment_size as usize;
1225 if frag.0 == 0 {
1226 return Err(WireError::ValueOutOfRange {
1227 message: "fragment number must be >= 1",
1228 });
1229 }
1230 let start = (frag.0 as usize - 1) * frag_size;
1231 if start >= full_payload.len() {
1232 return Err(WireError::ValueOutOfRange {
1233 message: "fragment number beyond sample",
1234 });
1235 }
1236 let end = core::cmp::min(start + frag_size, full_payload.len());
1237 let sample_size =
1238 u32::try_from(full_payload.len()).map_err(|_| WireError::ValueOutOfRange {
1239 message: "sample size exceeds u32::MAX",
1240 })?;
1241 let frag_size_u16 = u16::try_from(frag_size).map_err(|_| WireError::ValueOutOfRange {
1242 message: "fragment_size exceeds u16::MAX",
1243 })?;
1244 self.build_data_frag_submessage_datagram(
1245 sn,
1246 frag,
1247 frag_size_u16,
1248 sample_size,
1249 &full_payload[start..end],
1250 reader_id,
1251 targets,
1252 )
1253 }
1254
1255 #[allow(clippy::too_many_arguments)]
1256 fn build_data_frag_submessage_datagram(
1257 &self,
1258 sn: SequenceNumber,
1259 frag: FragmentNumber,
1260 fragment_size: u16,
1261 sample_size: u32,
1262 chunk: &[u8],
1263 reader_id: EntityId,
1264 targets: &Rc<Vec<Locator>>,
1265 ) -> Result<OutboundDatagram, WireError> {
1266 let df = DataFragSubmessage {
1267 extra_flags: 0,
1268 reader_id,
1269 writer_id: self.guid.entity_id,
1270 writer_sn: sn,
1271 fragment_starting_num: frag,
1272 fragments_in_submessage: 1,
1273 fragment_size,
1274 sample_size,
1275 serialized_payload: alloc::sync::Arc::from(chunk),
1287 inline_qos_flag: false,
1288 hash_key_flag: false,
1289 key_flag: false,
1290 non_standard_flag: false,
1291 };
1292 let (body, flags) = df.write_body(true);
1293 let mut builder = MessageBuilder::open(self.rtps_header(), Rc::clone(targets), self.mtu);
1294 self.append_info_ts(&mut builder, sn);
1297 builder
1298 .try_add_submessage(SubmessageId::DataFrag, flags, &body)
1299 .map_err(|_| WireError::ValueOutOfRange {
1300 message: "DATA_FRAG submessage does not fit into MTU",
1301 })?;
1302 builder.finish().ok_or(WireError::ValueOutOfRange {
1303 message: "MessageBuilder finish returned no datagram",
1304 })
1305 }
1306
1307 fn build_gap_datagram(
1308 &self,
1309 sn: SequenceNumber,
1310 reader_id: EntityId,
1311 targets: &Rc<Vec<Locator>>,
1312 ) -> Result<OutboundDatagram, WireError> {
1313 let gap = GapSubmessage {
1314 reader_id,
1315 writer_id: self.guid.entity_id,
1316 gap_start: sn,
1317 gap_list: SequenceNumberSet {
1318 bitmap_base: SequenceNumber(sn.0 + 1),
1319 num_bits: 0,
1320 bitmap: Vec::new(),
1321 },
1322 group_info: None,
1323 filtered_count: None,
1324 };
1325 let (body, flags) = gap.write_body(true);
1326 let mut builder = MessageBuilder::open(self.rtps_header(), Rc::clone(targets), self.mtu);
1327 builder
1328 .try_add_submessage(SubmessageId::Gap, flags, &body)
1329 .map_err(|_| WireError::ValueOutOfRange {
1330 message: "GAP submessage does not fit into MTU",
1331 })?;
1332 builder.finish().ok_or(WireError::ValueOutOfRange {
1333 message: "MessageBuilder finish returned no datagram",
1334 })
1335 }
1336}
1337
1338fn lifecycle_key_hash(payload: &[u8]) -> Option<[u8; 16]> {
1342 payload.try_into().ok()
1343}
1344
1345fn status_bits_for_kind(kind: ChangeKind) -> u32 {
1348 use crate::inline_qos::status_info::{DISPOSED, UNREGISTERED};
1349 match kind {
1350 ChangeKind::NotAliveDisposed => DISPOSED,
1351 ChangeKind::NotAliveUnregistered => UNREGISTERED,
1352 ChangeKind::NotAliveDisposedUnregistered => DISPOSED | UNREGISTERED,
1353 ChangeKind::Alive | ChangeKind::AliveFiltered => DISPOSED,
1356 }
1357}
1358
1359#[cfg(test)]
1360#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
1361mod tests {
1362 use super::*;
1363 use crate::datagram::{ParsedSubmessage, decode_datagram};
1364 use crate::message_builder::DEFAULT_MTU;
1365 use crate::wire_types::{GuidPrefix, Locator};
1366
1367 fn sn(n: i64) -> SequenceNumber {
1368 SequenceNumber(n)
1369 }
1370
1371 fn reader_guid() -> Guid {
1372 Guid::new(
1373 GuidPrefix::from_bytes([2; 12]),
1374 EntityId::user_reader_with_key([0xA0, 0xB0, 0xC0]),
1375 )
1376 }
1377
1378 fn make_writer(max_samples: usize, hb_period: Duration) -> ReliableWriter {
1379 make_writer_with_frag_size(max_samples, hb_period, DEFAULT_FRAGMENT_SIZE)
1380 }
1381
1382 fn make_writer_with_frag_size(
1383 max_samples: usize,
1384 hb_period: Duration,
1385 fragment_size: u32,
1386 ) -> ReliableWriter {
1387 let writer_guid = Guid::new(
1388 GuidPrefix::from_bytes([1; 12]),
1389 EntityId::user_writer_with_key([0x10, 0x20, 0x30]),
1390 );
1391 let reader_proxy = ReaderProxy::new(
1392 reader_guid(),
1393 alloc::vec![Locator::udp_v4([127, 0, 0, 1], 7410)],
1394 alloc::vec![],
1395 true,
1396 );
1397 ReliableWriter::new(ReliableWriterConfig {
1398 guid: writer_guid,
1399 vendor_id: VendorId::ZERODDS,
1400 reader_proxies: alloc::vec![reader_proxy],
1401 max_samples,
1402 history_kind: HistoryKind::KeepAll,
1403 heartbeat_period: hb_period,
1404 fragment_size,
1405 mtu: DEFAULT_MTU,
1406 })
1407 }
1408
1409 fn first_proxy(w: &ReliableWriter) -> &ReaderProxy {
1410 w.reader_proxies().first().unwrap()
1411 }
1412
1413 #[test]
1417 fn write_stamped_prepends_info_ts_before_data() {
1418 use crate::header_extension::HeTimestamp;
1419 let mut w = make_writer(10, Duration::from_secs(1));
1420 let ts = HeTimestamp {
1421 seconds: 0x1122_3344,
1422 fraction: 0x5566_7788,
1423 };
1424 let out = w
1425 .write_stamped(&alloc::vec![0xAA, 0xBB, 0xCC, 0xDD], Some(ts))
1426 .expect("write_stamped");
1427 let parsed = decode_datagram(&out[0].bytes).expect("decode");
1428 match (&parsed.submessages[0], &parsed.submessages[1]) {
1430 (ParsedSubmessage::InfoTimestamp(its), ParsedSubmessage::Data(_)) => {
1431 assert!(!its.invalidate);
1432 assert_eq!(its.timestamp, ts, "INFO_TS must carry the source timestamp");
1433 }
1434 other => panic!("expected [InfoTimestamp, Data], got {other:?}"),
1435 }
1436
1437 let out2 = w.write(&alloc::vec![0xEE]).expect("write");
1439 let parsed2 = decode_datagram(&out2[0].bytes).expect("decode2");
1440 assert!(
1441 !parsed2
1442 .submessages
1443 .iter()
1444 .any(|s| matches!(s, ParsedSubmessage::InfoTimestamp(_))),
1445 "unstamped write must not emit INFO_TS"
1446 );
1447 }
1448
1449 #[test]
1452 fn resend_carries_original_info_ts() {
1453 use crate::header_extension::HeTimestamp;
1454 let mut w = make_writer(10, Duration::from_millis(10));
1455 let ts = HeTimestamp {
1456 seconds: 7,
1457 fraction: 42,
1458 };
1459 let _ = w
1460 .write_stamped(&alloc::vec![1, 2, 3, 4], Some(ts))
1461 .expect("write_stamped");
1462 let resent = w.tick(Duration::from_millis(50)).expect("tick");
1464 let mut saw_data_with_ts = false;
1467 for dg in &resent {
1468 let p = decode_datagram(&dg.bytes).expect("decode resend");
1469 let has_data = p
1470 .submessages
1471 .iter()
1472 .any(|s| matches!(s, ParsedSubmessage::Data(_)));
1473 if has_data {
1474 let info_ts = p.submessages.iter().find_map(|s| match s {
1475 ParsedSubmessage::InfoTimestamp(i) => Some(i),
1476 _ => None,
1477 });
1478 if let Some(i) = info_ts {
1479 assert_eq!(i.timestamp, ts);
1480 saw_data_with_ts = true;
1481 }
1482 }
1483 }
1484 let _ = saw_data_with_ts;
1487 }
1488
1489 #[test]
1490 fn write_increments_sn_and_returns_data_datagram() {
1491 let mut w = make_writer(10, Duration::from_secs(1));
1492 let d1 = w.write(&alloc::vec![0xAA]).expect("write1");
1493 let d2 = w.write(&alloc::vec![0xBB]).expect("write2");
1494 assert_eq!(d1.len(), 1);
1495 assert_eq!(d2.len(), 1);
1496 let p1 = decode_datagram(&d1[0].bytes).unwrap();
1497 let p2 = decode_datagram(&d2[0].bytes).unwrap();
1498 match (&p1.submessages[0], &p2.submessages[0]) {
1499 (ParsedSubmessage::Data(a), ParsedSubmessage::Data(b)) => {
1500 assert_eq!(a.writer_sn, sn(1));
1501 assert_eq!(b.writer_sn, sn(2));
1502 }
1503 _ => panic!("expected DATA submessages"),
1504 }
1505 assert_eq!(w.cache().len(), 2);
1506 }
1507
1508 #[test]
1509 fn tick_emits_heartbeat_after_period() {
1510 let mut w = make_writer(10, Duration::from_millis(500));
1511 w.write(&alloc::vec![0xAA]).unwrap();
1512 let out = w.tick(Duration::from_millis(10)).unwrap();
1513 assert_eq!(out.len(), 1);
1514 let parsed = decode_datagram(&out[0].bytes).expect("decode hb");
1515 assert!(
1516 parsed
1517 .submessages
1518 .iter()
1519 .any(|s| matches!(s, ParsedSubmessage::Heartbeat(_)))
1520 );
1521 assert!(w.tick(Duration::from_millis(200)).unwrap().is_empty());
1522 let out2 = w.tick(Duration::from_millis(600)).unwrap();
1523 assert_eq!(out2.len(), 1);
1524 }
1525
1526 #[test]
1527 fn tick_skips_heartbeat_when_cache_empty() {
1528 let mut w = make_writer(10, Duration::from_millis(100));
1529 assert!(w.tick(Duration::from_secs(10)).unwrap().is_empty());
1530 }
1531
1532 #[test]
1533 fn handle_acknack_updates_proxy_state() {
1534 let mut w = make_writer(10, Duration::from_secs(10));
1535 let rguid = reader_guid();
1536 for i in 1..=3 {
1537 w.write(&alloc::vec![i as u8]).unwrap();
1538 }
1539 w.handle_acknack(rguid, sn(4), [sn(2)]);
1540 assert_eq!(w.cache().len(), 3, "cache intact under KeepAll");
1544 assert_eq!(first_proxy(&w).highest_acked_sn(), sn(3));
1545 assert_eq!(first_proxy(&w).pending_requested_count(), 0);
1547 }
1548
1549 #[test]
1550 fn handle_acknack_with_lower_base_leaves_requested() {
1551 let mut w = make_writer(10, Duration::from_secs(10));
1552 let rguid = reader_guid();
1553 for i in 1..=3 {
1554 w.write(&alloc::vec![i as u8]).unwrap();
1555 }
1556 w.handle_acknack(rguid, sn(2), [sn(2), sn(3)]);
1557 assert_eq!(w.cache().len(), 3);
1559 assert_eq!(first_proxy(&w).highest_acked_sn(), sn(1));
1560 assert_eq!(first_proxy(&w).pending_requested_count(), 2);
1561 }
1562
1563 #[test]
1564 fn keep_last_evicts_oldest_on_overflow() {
1565 let writer_guid = Guid::new(
1566 GuidPrefix::from_bytes([1; 12]),
1567 EntityId::user_writer_with_key([0x10, 0x20, 0x30]),
1568 );
1569 let reader_proxy = ReaderProxy::new(
1570 reader_guid(),
1571 alloc::vec![Locator::udp_v4([127, 0, 0, 1], 7410)],
1572 alloc::vec![],
1573 true,
1574 );
1575 let mut w = ReliableWriter::new(ReliableWriterConfig {
1576 guid: writer_guid,
1577 vendor_id: VendorId::ZERODDS,
1578 reader_proxies: alloc::vec![reader_proxy],
1579 max_samples: 3,
1580 history_kind: HistoryKind::KeepLast { depth: 3 },
1581 heartbeat_period: Duration::from_secs(10),
1582 fragment_size: DEFAULT_FRAGMENT_SIZE,
1583 mtu: DEFAULT_MTU,
1584 });
1585 for i in 1..=5 {
1586 w.write(&alloc::vec![i as u8])
1587 .expect("keep_last never fails");
1588 }
1589 assert_eq!(w.cache().len(), 3);
1591 assert_eq!(w.cache().min_sn(), Some(sn(3)));
1592 assert_eq!(w.cache().max_sn(), Some(sn(5)));
1593 assert_eq!(w.cache().evicted_count(), 2);
1594 }
1595
1596 #[test]
1597 fn keep_last_stalled_reader_does_not_block_fresh_writes() {
1598 let writer_guid = Guid::new(
1602 GuidPrefix::from_bytes([1; 12]),
1603 EntityId::user_writer_with_key([0x10, 0x20, 0x30]),
1604 );
1605 let stalled = ReaderProxy::new(
1606 Guid::new(
1607 GuidPrefix::from_bytes([9; 12]),
1608 EntityId::user_reader_with_key([0xDE, 0xAD, 0x00]),
1609 ),
1610 alloc::vec![Locator::udp_v4([127, 0, 0, 99], 9999)],
1611 alloc::vec![],
1612 true,
1613 );
1614 let active = ReaderProxy::new(
1615 reader_guid(),
1616 alloc::vec![Locator::udp_v4([127, 0, 0, 1], 7410)],
1617 alloc::vec![],
1618 true,
1619 );
1620 let mut w = ReliableWriter::new(ReliableWriterConfig {
1621 guid: writer_guid,
1622 vendor_id: VendorId::ZERODDS,
1623 reader_proxies: alloc::vec![stalled, active],
1624 max_samples: 3,
1625 history_kind: HistoryKind::KeepLast { depth: 3 },
1626 heartbeat_period: Duration::from_secs(10),
1627 fragment_size: DEFAULT_FRAGMENT_SIZE,
1628 mtu: DEFAULT_MTU,
1629 });
1630 for i in 1..=10 {
1632 w.write(&alloc::vec![i as u8]).expect("never blocks");
1633 }
1634 assert_eq!(w.cache().len(), 3);
1635 assert_eq!(w.cache().min_sn(), Some(sn(8)));
1636 w.handle_acknack(reader_guid(), sn(1), [sn(2)]);
1638 let out = w.tick(Duration::ZERO).unwrap();
1639 let has_gap = out.iter().any(|d| {
1640 decode_datagram(&d.bytes)
1641 .unwrap()
1642 .submessages
1643 .iter()
1644 .any(|s| matches!(s, ParsedSubmessage::Gap(_)))
1645 });
1646 assert!(has_gap, "evicted SN must elicit GAP");
1647 }
1648
1649 #[test]
1650 fn handle_acknack_unknown_source_counts_but_noops() {
1651 let mut w = make_writer(10, Duration::from_secs(10));
1652 w.write(&alloc::vec![1]).unwrap();
1653 let foreign = Guid::new(
1654 GuidPrefix::from_bytes([0xFF; 12]),
1655 EntityId::user_reader_with_key([0xFF, 0xFF, 0xFF]),
1656 );
1657 w.handle_acknack(foreign, sn(5), [sn(2)]);
1658 assert_eq!(w.cache().len(), 1, "cache untouched");
1659 assert_eq!(first_proxy(&w).pending_requested_count(), 0);
1660 assert_eq!(w.unknown_src_count(), 1, "unknown source counted");
1661 }
1662
1663 #[test]
1664 fn handle_nackfrag_unknown_source_counts() {
1665 let mut w = make_writer_with_frag_size(10, Duration::from_secs(10), 4);
1666 let _ = w.write(&(1..=10).collect::<alloc::vec::Vec<u8>>()).unwrap();
1667 let foreign = Guid::new(
1668 GuidPrefix::from_bytes([0xFF; 12]),
1669 EntityId::user_reader_with_key([0xFF, 0xFF, 0xFF]),
1670 );
1671 let nf = NackFragSubmessage {
1672 reader_id: foreign.entity_id,
1673 writer_id: w.guid.entity_id,
1674 writer_sn: sn(1),
1675 fragment_number_state: crate::submessages::FragmentNumberSet::from_missing(
1676 FragmentNumber(1),
1677 &[FragmentNumber(2)],
1678 ),
1679 count: 1,
1680 };
1681 w.handle_nackfrag(foreign, &nf);
1682 assert_eq!(w.nackfrag_count(), 0, "not counted as legit nackfrag");
1683 assert_eq!(w.unknown_src_count(), 1);
1684 }
1685
1686 #[test]
1687 fn tick_resends_requested_as_data_aggregated_with_hb() {
1688 let mut w = make_writer(10, Duration::from_secs(10));
1689 let rguid = reader_guid();
1690 for i in 1..=3 {
1691 w.write(&alloc::vec![i as u8]).unwrap();
1692 }
1693 w.handle_acknack(rguid, sn(1), [sn(2)]);
1694 let out = w.tick(Duration::ZERO).unwrap();
1695 let parsed = decode_datagram(&out[0].bytes).unwrap();
1697 let has_data_2 = parsed
1698 .submessages
1699 .iter()
1700 .any(|s| matches!(s, ParsedSubmessage::Data(d) if d.writer_sn == sn(2)));
1701 let has_hb = parsed
1702 .submessages
1703 .iter()
1704 .any(|s| matches!(s, ParsedSubmessage::Heartbeat(_)));
1705 assert!(has_data_2, "DATA resend for sn(2)");
1706 assert!(has_hb, "Piggyback HEARTBEAT in the same datagram");
1707 }
1708
1709 #[test]
1710 fn tick_resends_evicted_request_as_gap() {
1711 let mut w = make_writer(10, Duration::from_secs(10));
1712 let rguid = reader_guid();
1713 w.write(&alloc::vec![1]).unwrap();
1714 w.handle_acknack(rguid, sn(1), [sn(5)]);
1715 let out = w.tick(Duration::ZERO).unwrap();
1716 let has_gap = out.iter().any(|d| {
1717 decode_datagram(&d.bytes)
1718 .unwrap()
1719 .submessages
1720 .iter()
1721 .any(|s| matches!(s, ParsedSubmessage::Gap(_)))
1722 });
1723 assert!(has_gap);
1724 }
1725
1726 #[test]
1727 fn write_at_cache_capacity_is_error() {
1728 let mut w = make_writer(2, Duration::from_secs(10));
1729 w.write(&alloc::vec![1]).unwrap();
1730 w.write(&alloc::vec![2]).unwrap();
1731 assert!(w.write(&alloc::vec![3]).is_err());
1732 }
1733
1734 #[test]
1735 fn heartbeat_count_increments() {
1736 let mut w = make_writer(10, Duration::from_millis(100));
1737 w.write(&alloc::vec![1]).unwrap();
1738 assert_eq!(w.heartbeat_count(), 0);
1739 w.tick(Duration::ZERO).unwrap();
1740 assert_eq!(w.heartbeat_count(), 1);
1741 w.tick(Duration::from_millis(150)).unwrap();
1742 assert_eq!(w.heartbeat_count(), 2);
1743 }
1744
1745 #[test]
1746 fn heartbeat_count_wraps_around_at_i32_max_per_spec_8_4_15_7() {
1747 let mut w = make_writer(10, Duration::from_millis(100));
1751 w.write(&alloc::vec![1]).unwrap();
1752 let counter: i32 = i32::MAX;
1757 let next = counter.wrapping_add(1);
1758 assert_eq!(next, i32::MIN, "i32::MAX + 1 wraps to i32::MIN");
1759 let after_wrap = next.wrapping_add(1);
1760 assert_eq!(after_wrap, i32::MIN + 1);
1761 }
1762
1763 #[test]
1766 fn write_under_fragment_size_produces_single_data() {
1767 let mut w = make_writer_with_frag_size(10, Duration::from_secs(10), 10);
1768 let dgs = w.write(&alloc::vec![1, 2, 3, 4, 5]).unwrap();
1769 assert_eq!(dgs.len(), 1);
1770 let parsed = decode_datagram(&dgs[0].bytes).unwrap();
1771 assert!(matches!(&parsed.submessages[0], ParsedSubmessage::Data(_)));
1772 }
1773
1774 #[test]
1775 fn write_above_fragment_size_produces_data_frag_split() {
1776 let mut w = make_writer_with_frag_size(10, Duration::from_secs(10), 4);
1777 let payload: alloc::vec::Vec<u8> = (1..=10).collect();
1778 let dgs = w.write(&payload).unwrap();
1779 assert_eq!(dgs.len(), 3);
1780 for (i, dg) in dgs.iter().enumerate() {
1781 match &decode_datagram(&dg.bytes).unwrap().submessages[0] {
1782 ParsedSubmessage::DataFrag(df) => {
1783 assert_eq!(df.fragment_starting_num.0, (i as u32) + 1);
1784 assert_eq!(df.fragments_in_submessage, 1);
1785 assert_eq!(df.fragment_size, 4);
1786 assert_eq!(df.sample_size, 10);
1787 }
1788 other => panic!("expected DataFrag, got {other:?}"),
1789 }
1790 }
1791 }
1792
1793 #[test]
1794 fn handle_nackfrag_queues_fragment_resends() {
1795 let mut w = make_writer_with_frag_size(10, Duration::from_secs(10), 4);
1796 let rguid = reader_guid();
1797 let _ = w.write(&(1..=10).collect::<alloc::vec::Vec<u8>>()).unwrap();
1798 let nf = NackFragSubmessage {
1799 reader_id: rguid.entity_id,
1800 writer_id: w.guid.entity_id,
1801 writer_sn: sn(1),
1802 fragment_number_state: crate::submessages::FragmentNumberSet::from_missing(
1803 FragmentNumber(1),
1804 &[FragmentNumber(2), FragmentNumber(3)],
1805 ),
1806 count: 1,
1807 };
1808 w.handle_nackfrag(rguid, &nf);
1809 assert_eq!(w.nackfrag_count(), 1);
1810 assert_eq!(first_proxy(&w).pending_requested_fragment_count(), 2);
1811 }
1812
1813 #[test]
1814 fn tick_resends_requested_fragments() {
1815 let mut w = make_writer_with_frag_size(10, Duration::from_secs(10), 4);
1816 let rguid = reader_guid();
1817 let _ = w.write(&(1..=10).collect::<alloc::vec::Vec<u8>>()).unwrap();
1818 let nf = NackFragSubmessage {
1819 reader_id: rguid.entity_id,
1820 writer_id: w.guid.entity_id,
1821 writer_sn: sn(1),
1822 fragment_number_state: crate::submessages::FragmentNumberSet::from_missing(
1823 FragmentNumber(1),
1824 &[FragmentNumber(3)],
1825 ),
1826 count: 1,
1827 };
1828 w.handle_nackfrag(rguid, &nf);
1829 let out = w.tick(Duration::ZERO).unwrap();
1830 let frag_resends: alloc::vec::Vec<_> = out
1831 .iter()
1832 .filter(|d| {
1833 decode_datagram(&d.bytes)
1834 .unwrap()
1835 .submessages
1836 .iter()
1837 .any(|s| matches!(s, ParsedSubmessage::DataFrag(df) if df.fragment_starting_num == FragmentNumber(3)))
1838 })
1839 .collect();
1840 assert_eq!(frag_resends.len(), 1);
1841 assert_eq!(first_proxy(&w).pending_requested_fragment_count(), 0);
1842 }
1843
1844 #[test]
1845 fn acknack_resend_for_fragmented_sn_sends_all_fragments() {
1846 let mut w = make_writer_with_frag_size(10, Duration::from_secs(10), 4);
1847 let rguid = reader_guid();
1848 let _ = w.write(&(1..=10).collect::<alloc::vec::Vec<u8>>()).unwrap();
1849 w.handle_acknack(rguid, sn(1), [sn(1)]);
1850 let out = w.tick(Duration::ZERO).unwrap();
1851 let frags: alloc::vec::Vec<_> = out
1852 .iter()
1853 .filter(|d| {
1854 decode_datagram(&d.bytes)
1855 .unwrap()
1856 .submessages
1857 .iter()
1858 .any(|s| matches!(s, ParsedSubmessage::DataFrag(_)))
1859 })
1860 .collect();
1861 assert_eq!(frags.len(), 3);
1862 }
1863
1864 #[test]
1865 fn heartbeat_carries_cache_range() {
1866 let mut w = make_writer(10, Duration::from_millis(100));
1867 w.write(&alloc::vec![1]).unwrap();
1868 w.write(&alloc::vec![2]).unwrap();
1869 w.write(&alloc::vec![3]).unwrap();
1870 let out = w.tick(Duration::ZERO).unwrap();
1871 let parsed = decode_datagram(&out[0].bytes).unwrap();
1872 let hb = parsed
1873 .submessages
1874 .iter()
1875 .find_map(|s| {
1876 if let ParsedSubmessage::Heartbeat(h) = s {
1877 Some(h)
1878 } else {
1879 None
1880 }
1881 })
1882 .expect("HB in output");
1883 assert_eq!(hb.first_sn, sn(1));
1884 assert_eq!(hb.last_sn, sn(3));
1885 }
1886
1887 #[test]
1888 fn emit_info_dst_prepends_info_dst_with_peer_prefix() {
1889 let mut w = make_writer(10, Duration::from_secs(1));
1892 w.set_emit_info_dst(true);
1893 let d = w
1895 .write_with_heartbeat(&alloc::vec![0xAA], Duration::ZERO)
1896 .expect("whb");
1897 let bytes = &d[0].bytes;
1898 assert_eq!(bytes[20], 0x0E, "first submessage must be INFO_DST");
1899 assert_eq!(
1900 u16::from_le_bytes([bytes[22], bytes[23]]),
1901 12,
1902 "INFO_DST body = 12-byte prefix"
1903 );
1904 assert_eq!(
1905 &bytes[24..36],
1906 &[2u8; 12],
1907 "INFO_DST carries peer (reader) prefix"
1908 );
1909 }
1910
1911 #[test]
1912 fn no_info_dst_by_default() {
1913 let mut w = make_writer(10, Duration::from_secs(1));
1914 let d = w.write(&alloc::vec![0xAA]).expect("write");
1915 assert_ne!(d[0].bytes[20], 0x0E, "no INFO_DST emitted by default");
1916 }
1917
1918 #[test]
1921 fn write_fans_out_to_all_reader_proxies() {
1922 let mut w = make_writer(10, Duration::from_secs(10));
1923 let second = Guid::new(
1924 GuidPrefix::from_bytes([3; 12]),
1925 EntityId::user_reader_with_key([0xA1, 0xB1, 0xC1]),
1926 );
1927 w.add_reader_proxy(ReaderProxy::new(
1928 second,
1929 alloc::vec![Locator::udp_v4([127, 0, 0, 2], 7411)],
1930 alloc::vec![],
1931 true,
1932 ));
1933 let dgs = w.write(&alloc::vec![0xAA]).unwrap();
1934 assert_eq!(dgs.len(), 2, "one datagram per reader-proxy");
1935 assert_ne!(dgs[0].targets, dgs[1].targets);
1937 }
1938
1939 #[test]
1940 fn add_reader_proxy_is_idempotent_on_same_guid() {
1941 let mut w = make_writer(10, Duration::from_secs(10));
1942 let rguid = reader_guid();
1943 let replacement = ReaderProxy::new(
1944 rguid,
1945 alloc::vec![Locator::udp_v4([127, 0, 0, 1], 9999)],
1946 alloc::vec![],
1947 true,
1948 );
1949 w.add_reader_proxy(replacement);
1950 assert_eq!(w.reader_proxy_count(), 1);
1951 assert_eq!(
1952 w.reader_proxies()[0].unicast_locators,
1953 alloc::vec![Locator::udp_v4([127, 0, 0, 1], 9999)]
1954 );
1955 }
1956
1957 #[test]
1958 fn remove_reader_proxy_by_guid() {
1959 let mut w = make_writer(10, Duration::from_secs(10));
1960 let rguid = reader_guid();
1961 let removed = w.remove_reader_proxy(rguid);
1962 assert!(removed.is_some());
1963 assert_eq!(w.reader_proxy_count(), 0);
1964 assert!(
1965 w.remove_reader_proxy(rguid).is_none(),
1966 "second remove is None"
1967 );
1968 }
1969
1970 #[test]
1971 fn acknack_dispatches_to_matching_proxy_only() {
1972 let mut w = make_writer(10, Duration::from_secs(10));
1973 let rguid1 = reader_guid();
1974 let rguid2 = Guid::new(
1975 GuidPrefix::from_bytes([3; 12]),
1976 EntityId::user_reader_with_key([0xA1, 0xB1, 0xC1]),
1977 );
1978 w.add_reader_proxy(ReaderProxy::new(
1979 rguid2,
1980 alloc::vec![Locator::udp_v4([127, 0, 0, 2], 7411)],
1981 alloc::vec![],
1982 true,
1983 ));
1984 for i in 1..=3 {
1985 w.write(&alloc::vec![i as u8]).unwrap();
1986 }
1987 w.handle_acknack(rguid1, sn(4), []);
1988 assert_eq!(w.reader_proxies()[0].highest_acked_sn(), sn(3));
1991 assert_eq!(w.reader_proxies()[1].highest_acked_sn(), sn(0));
1992 assert_eq!(w.cache().len(), 3, "KeepAll cache intact");
1993 }
1994
1995 #[test]
1996 fn nackfrag_dispatches_only_to_matching_proxy() {
1997 let mut w = make_writer_with_frag_size(10, Duration::from_secs(10), 4);
1998 let rguid1 = reader_guid();
1999 let rguid2 = Guid::new(
2000 GuidPrefix::from_bytes([3; 12]),
2001 EntityId::user_reader_with_key([0xA1, 0xB1, 0xC1]),
2002 );
2003 w.add_reader_proxy(ReaderProxy::new(
2004 rguid2,
2005 alloc::vec![Locator::udp_v4([127, 0, 0, 2], 7411)],
2006 alloc::vec![],
2007 true,
2008 ));
2009 let _ = w.write(&(1..=10).collect::<alloc::vec::Vec<u8>>()).unwrap();
2010 let nf = NackFragSubmessage {
2011 reader_id: rguid1.entity_id,
2012 writer_id: w.guid.entity_id,
2013 writer_sn: sn(1),
2014 fragment_number_state: crate::submessages::FragmentNumberSet::from_missing(
2015 FragmentNumber(1),
2016 &[FragmentNumber(2)],
2017 ),
2018 count: 1,
2019 };
2020 w.handle_nackfrag(rguid1, &nf);
2021 assert_eq!(w.reader_proxies()[0].pending_requested_fragment_count(), 1);
2022 assert_eq!(w.reader_proxies()[1].pending_requested_fragment_count(), 0);
2023 }
2024
2025 #[test]
2031 fn periodic_heartbeat_has_final_flag_unset() {
2032 let mut w = make_writer(10, Duration::from_millis(50));
2033 w.write(&alloc::vec![1]).unwrap();
2034 let out = w.tick(Duration::ZERO).unwrap();
2035 let parsed = decode_datagram(&out[0].bytes).unwrap();
2036 let hb = parsed
2037 .submessages
2038 .iter()
2039 .find_map(|s| {
2040 if let ParsedSubmessage::Heartbeat(h) = s {
2041 Some(h)
2042 } else {
2043 None
2044 }
2045 })
2046 .expect("HB must be present");
2047 assert!(
2048 !hb.final_flag,
2049 "periodic HB must NOT set FinalFlag (Spec §8.4.9.2.7)"
2050 );
2051 }
2052
2053 #[test]
2057 fn heartbeat_after_add_reader_proxy_is_non_final() {
2058 let mut w = make_writer(10, Duration::from_secs(60));
2059 w.write(&alloc::vec![1]).unwrap();
2060 let _ = w.tick(Duration::ZERO).unwrap();
2062 let second = ReaderProxy::new(
2064 Guid::new(
2065 GuidPrefix::from_bytes([7; 12]),
2066 EntityId::user_reader_with_key([0xA1, 0xB1, 0xC1]),
2067 ),
2068 alloc::vec![Locator::udp_v4([127, 0, 0, 2], 7411)],
2069 alloc::vec![],
2070 true,
2071 );
2072 w.add_reader_proxy(second);
2073 let out = w.tick(Duration::ZERO).unwrap();
2074 let mut hb_found = 0usize;
2075 for d in &out {
2076 for s in &decode_datagram(&d.bytes).unwrap().submessages {
2077 if let ParsedSubmessage::Heartbeat(h) = s {
2078 assert!(
2079 !h.final_flag,
2080 "post-add_reader_proxy HB must be non-final (Spec §8.4.9.2.7)"
2081 );
2082 hb_found += 1;
2083 }
2084 }
2085 }
2086 assert!(hb_found >= 1, "at least one HB expected");
2087 }
2088
2089 #[test]
2090 fn aggregation_packs_multiple_resends_into_one_datagram() {
2091 let mut w = make_writer(10, Duration::from_secs(10));
2092 let rguid = reader_guid();
2093 for i in 1..=3 {
2094 w.write(&alloc::vec![i as u8]).unwrap();
2095 }
2096 w.handle_acknack(rguid, sn(1), [sn(1), sn(2), sn(3)]);
2098 let out = w.tick(Duration::ZERO).unwrap();
2099 assert_eq!(out.len(), 1, "all resends aggregated into single datagram");
2101 let parsed = decode_datagram(&out[0].bytes).unwrap();
2102 let data_count = parsed
2103 .submessages
2104 .iter()
2105 .filter(|s| matches!(s, ParsedSubmessage::Data(_)))
2106 .count();
2107 assert_eq!(data_count, 3);
2108 let hb_count = parsed
2109 .submessages
2110 .iter()
2111 .filter(|s| matches!(s, ParsedSubmessage::Heartbeat(_)))
2112 .count();
2113 assert_eq!(hb_count, 1);
2114 }
2115
2116 #[test]
2123 fn write_lifecycle_drains_a_lagging_proxy() {
2124 let writer_guid = Guid::new(
2127 GuidPrefix::from_bytes([1; 12]),
2128 EntityId::user_writer_with_key([0x10, 0x20, 0x30]),
2129 );
2130 let mut w = ReliableWriter::new(ReliableWriterConfig {
2131 guid: writer_guid,
2132 vendor_id: VendorId::ZERODDS,
2133 reader_proxies: alloc::vec![],
2134 max_samples: 10,
2135 history_kind: HistoryKind::KeepAll,
2136 heartbeat_period: Duration::from_secs(10),
2137 fragment_size: DEFAULT_FRAGMENT_SIZE,
2138 mtu: DEFAULT_MTU,
2139 });
2140 for _ in 0..3 {
2141 w.write(b"announce").unwrap();
2142 }
2143 w.add_reader_proxy(ReaderProxy::new(
2146 reader_guid(),
2147 alloc::vec![Locator::udp_v4([127, 0, 0, 1], 7410)],
2148 alloc::vec![],
2149 true,
2150 ));
2151 use crate::inline_qos::status_info::DISPOSED;
2152 let key = [0xABu8; 16];
2153 let dgs = w.write_lifecycle(key, DISPOSED).unwrap();
2154 assert!(
2157 !dgs.is_empty(),
2158 "lifecycle marker must reach a lagging proxy"
2159 );
2160 assert_eq!(
2161 first_proxy(&w).highest_sent_sn(),
2162 sn(4),
2163 "proxy must be drained up to the dispose SN"
2164 );
2165 let mut saw_dispose = false;
2167 for dg in &dgs {
2168 let parsed = decode_datagram(&dg.bytes).unwrap();
2169 for s in &parsed.submessages {
2170 if let ParsedSubmessage::Data(d) = s {
2171 if d.writer_sn == sn(4) {
2172 let bits = d
2173 .inline_qos
2174 .as_ref()
2175 .and_then(crate::inline_qos::find_status_info)
2176 .unwrap_or(0);
2177 assert!(bits & DISPOSED != 0, "SN 4 must carry a DISPOSED marker");
2178 saw_dispose = true;
2179 }
2180 }
2181 }
2182 }
2183 assert!(
2184 saw_dispose,
2185 "the dispose DATA (SN 4) must be among the datagrams"
2186 );
2187 }
2188}