1use crate::config::SubscriptionConfig;
2use crate::error::McrxError;
3use crate::packet::{Packet, PacketWithMetadata};
4use crate::platform::{
5 ReceiveSocket, join_multicast_group, leave_multicast_group, open_bound_socket,
6 prepare_existing_socket,
7};
8use crate::subscription::{Subscription, SubscriptionId};
9use socket2::Socket;
10
11#[cfg(feature = "metrics")]
12use crate::metrics::ContextMetricsSnapshot;
13#[cfg(feature = "metrics")]
14use std::sync::atomic::{AtomicU64, Ordering};
15#[cfg(feature = "metrics")]
16use std::time::SystemTime;
17
18#[cfg(feature = "metrics")]
19#[derive(Debug, Default)]
20struct ContextMetricsInner {
21 subscriptions_added: AtomicU64,
22 subscriptions_removed: AtomicU64,
23 total_packets_received: AtomicU64,
24 total_bytes_received: AtomicU64,
25 total_would_block_count: AtomicU64,
26 total_receive_errors: AtomicU64,
27 total_join_count: AtomicU64,
28 total_leave_count: AtomicU64,
29 batch_calls: AtomicU64,
30 batch_packets_received: AtomicU64,
31}
32
33#[derive(Debug)]
35pub struct Context {
36 subscriptions: Vec<Subscription>,
37 next_subscription_id: u64,
38 next_recv_index: usize,
39 #[cfg(feature = "metrics")]
40 metrics: ContextMetricsInner,
41}
42
43const MAX_DRAIN_PACKETS_PER_CALL: usize = 4096;
44
45impl Default for Context {
46 fn default() -> Self {
47 Self::new()
48 }
49}
50
51impl Context {
52 #[cfg(feature = "metrics")]
53 fn record_received_packet(&self, payload_len: usize) {
54 self.metrics
55 .total_packets_received
56 .fetch_add(1, Ordering::Relaxed);
57 self.metrics
58 .total_bytes_received
59 .fetch_add(payload_len as u64, Ordering::Relaxed);
60 }
61
62 #[cfg(feature = "metrics")]
63 fn record_would_block(&self) {
64 self.metrics
65 .total_would_block_count
66 .fetch_add(1, Ordering::Relaxed);
67 }
68
69 #[cfg(feature = "metrics")]
70 fn record_receive_error(&self) {
71 self.metrics
72 .total_receive_errors
73 .fetch_add(1, Ordering::Relaxed);
74 }
75
76 #[cfg(feature = "metrics")]
77 fn record_batch_call(&self) {
78 self.metrics.batch_calls.fetch_add(1, Ordering::Relaxed);
79 }
80
81 #[cfg(feature = "metrics")]
82 fn record_batch_packets_received(&self, packet_count: usize) {
83 self.metrics
84 .batch_packets_received
85 .fetch_add(packet_count as u64, Ordering::Relaxed);
86 }
87
88 fn ensure_subscription_config_is_unique(
89 &self,
90 config: &SubscriptionConfig,
91 ) -> Result<(), McrxError> {
92 if self
93 .subscriptions
94 .iter()
95 .any(|subscription| subscription.config() == config)
96 {
97 return Err(McrxError::DuplicateSubscription);
98 }
99
100 Ok(())
101 }
102
103 fn insert_subscription(
104 &mut self,
105 config: SubscriptionConfig,
106 socket: ReceiveSocket,
107 ) -> SubscriptionId {
108 let id = SubscriptionId(self.next_subscription_id);
109 self.next_subscription_id += 1;
110
111 let subscription = Subscription::from_receive_socket(id, config, socket);
112 self.subscriptions.push(subscription);
113
114 #[cfg(feature = "metrics")]
115 self.metrics
116 .subscriptions_added
117 .fetch_add(1, Ordering::Relaxed);
118
119 id
120 }
121
122 fn finish_subscription_removal(&mut self, index: usize) -> Subscription {
123 let removed = self.subscriptions.swap_remove(index);
124
125 if self.subscriptions.is_empty() {
126 self.next_recv_index = 0;
127 } else {
128 self.next_recv_index %= self.subscriptions.len();
129 }
130
131 #[cfg(feature = "metrics")]
132 self.metrics
133 .subscriptions_removed
134 .fetch_add(1, Ordering::Relaxed);
135
136 removed
137 }
138
139 pub fn new() -> Self {
141 Self {
142 subscriptions: Vec::new(),
143 next_subscription_id: 1,
144 next_recv_index: 0,
145 #[cfg(feature = "metrics")]
146 metrics: ContextMetricsInner::default(),
147 }
148 }
149
150 pub fn subscription_count(&self) -> usize {
152 self.subscriptions.len()
153 }
154
155 #[cfg(feature = "metrics")]
161 pub fn metrics_snapshot(&self) -> ContextMetricsSnapshot {
162 let mut joined_subscriptions = 0usize;
163
164 for subscription in &self.subscriptions {
165 if subscription.is_joined() {
166 joined_subscriptions += 1;
167 }
168 }
169
170 ContextMetricsSnapshot {
171 subscriptions_added: self.metrics.subscriptions_added.load(Ordering::Relaxed),
172 subscriptions_removed: self.metrics.subscriptions_removed.load(Ordering::Relaxed),
173 active_subscriptions: self.subscriptions.len(),
174 joined_subscriptions,
175 total_packets_received: self.metrics.total_packets_received.load(Ordering::Relaxed),
176 total_bytes_received: self.metrics.total_bytes_received.load(Ordering::Relaxed),
177 total_would_block_count: self.metrics.total_would_block_count.load(Ordering::Relaxed),
178 total_receive_errors: self.metrics.total_receive_errors.load(Ordering::Relaxed),
179 total_join_count: self.metrics.total_join_count.load(Ordering::Relaxed),
180 total_leave_count: self.metrics.total_leave_count.load(Ordering::Relaxed),
181 batch_calls: self.metrics.batch_calls.load(Ordering::Relaxed),
182 batch_packets_received: self.metrics.batch_packets_received.load(Ordering::Relaxed),
183 captured_at: SystemTime::now(),
184 }
185 }
186
187 pub fn contains_subscription(&self, id: SubscriptionId) -> bool {
189 self.subscriptions
190 .iter()
191 .any(|subscription| subscription.id() == id)
192 }
193
194 pub fn get_subscription(&self, id: SubscriptionId) -> Option<&Subscription> {
196 self.subscriptions
197 .iter()
198 .find(|subscription| subscription.id() == id)
199 }
200
201 pub fn get_subscription_mut(&mut self, id: SubscriptionId) -> Option<&mut Subscription> {
203 self.subscriptions
204 .iter_mut()
205 .find(|subscription| subscription.id() == id)
206 }
207
208 pub fn add_subscription(
216 &mut self,
217 config: SubscriptionConfig,
218 ) -> Result<SubscriptionId, McrxError> {
219 config.validate()?;
220 self.ensure_subscription_config_is_unique(&config)?;
221
222 let socket = open_bound_socket(&config)?;
223 Ok(self.insert_subscription(config, socket))
224 }
225
226 pub fn add_subscription_with_socket(
236 &mut self,
237 config: SubscriptionConfig,
238 socket: Socket,
239 ) -> Result<SubscriptionId, McrxError> {
240 config.validate()?;
241 self.ensure_subscription_config_is_unique(&config)?;
242
243 let socket = prepare_existing_socket(socket, &config)?;
244
245 Ok(self.insert_subscription(config, socket))
246 }
247
248 pub fn remove_subscription(&mut self, id: SubscriptionId) -> bool {
255 self.take_subscription(id).is_some()
256 }
257
258 pub fn take_subscription(&mut self, id: SubscriptionId) -> Option<Subscription> {
265 let index = self
266 .subscriptions
267 .iter()
268 .position(|subscription| subscription.id() == id)?;
269
270 Some(self.finish_subscription_removal(index))
271 }
272
273 pub fn join_subscription(&mut self, id: SubscriptionId) -> Result<(), McrxError> {
275 let subscription = self
276 .get_subscription_mut(id)
277 .ok_or(McrxError::SubscriptionNotFound)?;
278
279 if subscription.is_joined() {
280 return Err(McrxError::SubscriptionAlreadyJoined);
281 }
282
283 join_multicast_group(subscription.socket(), subscription.config())?;
284 subscription.mark_joined()?;
285
286 #[cfg(feature = "metrics")]
287 self.metrics
288 .total_join_count
289 .fetch_add(1, Ordering::Relaxed);
290
291 Ok(())
292 }
293
294 fn try_recv_from_joined<T>(
295 &mut self,
296 mut recv: impl FnMut(&Subscription) -> Result<Option<T>, McrxError>,
297 _packet_len: impl Fn(&T) -> usize,
298 ) -> Result<Option<T>, McrxError> {
299 let subscription_count = self.subscriptions.len();
300
301 if subscription_count == 0 {
302 return Ok(None);
303 }
304
305 let mut index = self.next_recv_index;
306
307 for _ in 0..subscription_count {
308 let subscription = &self.subscriptions[index];
309
310 if !subscription.is_joined() {
311 index += 1;
312 if index == subscription_count {
313 index = 0;
314 }
315 continue;
316 }
317
318 match recv(subscription) {
319 Ok(Some(packet)) => {
320 self.next_recv_index = (index + 1) % subscription_count;
321
322 #[cfg(feature = "metrics")]
323 self.record_received_packet(_packet_len(&packet));
324
325 return Ok(Some(packet));
326 }
327 Ok(None) => {
328 #[cfg(feature = "metrics")]
329 self.record_would_block();
330 }
331 Err(err) => {
332 #[cfg(feature = "metrics")]
333 self.record_receive_error();
334
335 return Err(err);
336 }
337 }
338
339 index += 1;
340 if index == subscription_count {
341 index = 0;
342 }
343 }
344
345 Ok(None)
346 }
347
348 pub fn leave_subscription(&mut self, id: SubscriptionId) -> Result<(), McrxError> {
350 let subscription = self
351 .get_subscription_mut(id)
352 .ok_or(McrxError::SubscriptionNotFound)?;
353
354 if !subscription.is_joined() {
355 return Err(McrxError::SubscriptionNotJoined);
356 }
357
358 leave_multicast_group(subscription.socket(), subscription.config())?;
359 subscription.mark_bound()?;
360
361 #[cfg(feature = "metrics")]
362 self.metrics
363 .total_leave_count
364 .fetch_add(1, Ordering::Relaxed);
365
366 Ok(())
367 }
368
369 pub fn subscriptions(&self) -> &[Subscription] {
371 &self.subscriptions
372 }
373
374 pub fn subscriptions_mut(&mut self) -> &mut [Subscription] {
376 &mut self.subscriptions
377 }
378
379 pub fn try_recv_any(&mut self) -> Result<Option<Packet>, McrxError> {
387 self.try_recv_from_joined(Subscription::try_recv, |packet| packet.payload.len())
388 }
389
390 pub fn try_recv_any_with_metadata(&mut self) -> Result<Option<PacketWithMetadata>, McrxError> {
395 self.try_recv_from_joined(Subscription::try_recv_with_metadata, |packet| {
396 packet.packet.payload.len()
397 })
398 }
399
400 pub fn try_recv_batch_into(
412 &mut self,
413 out: &mut Vec<Packet>,
414 max_packets: usize,
415 ) -> Result<usize, McrxError> {
416 #[cfg(feature = "metrics")]
417 self.record_batch_call();
418
419 self.try_recv_batch_into_impl(out, max_packets)
420 }
421
422 fn try_recv_batch_into_impl(
423 &mut self,
424 out: &mut Vec<Packet>,
425 max_packets: usize,
426 ) -> Result<usize, McrxError> {
427 let mut received = 0;
428
429 for _ in 0..max_packets {
430 match self.try_recv_any()? {
431 Some(packet) => {
432 out.push(packet);
433 received += 1;
434 }
435 None => break,
436 }
437 }
438
439 #[cfg(feature = "metrics")]
440 self.record_batch_packets_received(received);
441
442 Ok(received)
443 }
444
445 pub fn try_recv_batch_with_metadata_into(
448 &mut self,
449 out: &mut Vec<PacketWithMetadata>,
450 max_packets: usize,
451 ) -> Result<usize, McrxError> {
452 #[cfg(feature = "metrics")]
453 self.record_batch_call();
454
455 self.try_recv_batch_with_metadata_into_impl(out, max_packets)
456 }
457
458 fn try_recv_batch_with_metadata_into_impl(
459 &mut self,
460 out: &mut Vec<PacketWithMetadata>,
461 max_packets: usize,
462 ) -> Result<usize, McrxError> {
463 let mut received = 0;
464
465 for _ in 0..max_packets {
466 match self.try_recv_any_with_metadata()? {
467 Some(packet) => {
468 out.push(packet);
469 received += 1;
470 }
471 None => break,
472 }
473 }
474
475 #[cfg(feature = "metrics")]
476 self.record_batch_packets_received(received);
477
478 Ok(received)
479 }
480
481 pub fn try_recv_all_into(&mut self, out: &mut Vec<Packet>) -> Result<usize, McrxError> {
489 #[cfg(feature = "metrics")]
490 self.record_batch_call();
491
492 self.try_recv_batch_into_impl(out, MAX_DRAIN_PACKETS_PER_CALL)
493 }
494
495 pub fn try_recv_all_with_metadata_into(
498 &mut self,
499 out: &mut Vec<PacketWithMetadata>,
500 ) -> Result<usize, McrxError> {
501 #[cfg(feature = "metrics")]
502 self.record_batch_call();
503
504 self.try_recv_batch_with_metadata_into_impl(out, MAX_DRAIN_PACKETS_PER_CALL)
505 }
506}
507
508#[cfg(test)]
509mod tests {
510 use super::*;
511 use crate::config::SourceFilter;
512 use crate::subscription::SubscriptionState;
513 use socket2::{Domain, Protocol, SockAddr, Socket, Type};
514 use std::net::{Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6};
515 use std::thread;
516 use std::time::{Duration, Instant};
517
518 use crate::test_support::{
519 ipv6_group_socket_addr, ipv6_multicast_loopback_available, make_multicast_sender,
520 make_multicast_sender_v6, recv_next_packet, sample_config_on_unused_port,
521 sample_config_v6_on_unused_port,
522 };
523
524 fn make_bound_external_socket(port: u16) -> Socket {
525 let socket = Socket::new(Domain::IPV4, Type::DGRAM, Some(Protocol::UDP)).unwrap();
526 socket.set_reuse_address(true).unwrap();
527 socket
528 .bind(&SockAddr::from(SocketAddrV4::new(
529 Ipv4Addr::UNSPECIFIED,
530 port,
531 )))
532 .unwrap();
533 socket
534 }
535
536 fn ipv4_group(config: &SubscriptionConfig) -> Ipv4Addr {
537 config.ipv4_membership().unwrap().group
538 }
539
540 fn ipv4_group_socket_addr(config: &SubscriptionConfig) -> SocketAddrV4 {
541 SocketAddrV4::new(ipv4_group(config), config.dst_port)
542 }
543
544 fn assert_pktinfo_metadata(
545 packet: &PacketWithMetadata,
546 expected_destination: std::net::IpAddr,
547 ) {
548 #[cfg(any(
549 target_os = "linux",
550 target_os = "android",
551 windows,
552 target_vendor = "apple",
553 target_os = "freebsd",
554 target_os = "dragonfly",
555 target_os = "netbsd",
556 target_os = "openbsd"
557 ))]
558 {
559 assert_eq!(
560 packet.metadata.destination_local_ip,
561 Some(expected_destination)
562 );
563 assert!(packet.metadata.ingress_interface_index.is_some());
564 }
565
566 #[cfg(not(any(
567 target_os = "linux",
568 target_os = "android",
569 windows,
570 target_vendor = "apple",
571 target_os = "freebsd",
572 target_os = "dragonfly",
573 target_os = "netbsd",
574 target_os = "openbsd"
575 )))]
576 {
577 let _ = expected_destination;
578 assert_eq!(packet.metadata.destination_local_ip, None);
579 assert_eq!(packet.metadata.ingress_interface_index, None);
580 }
581 }
582
583 fn send_round_robin_test_packets(
584 first_config: &SubscriptionConfig,
585 second_config: &SubscriptionConfig,
586 ) {
587 let sender = make_multicast_sender();
588
589 sender
590 .send_to(b"first-1", ipv4_group_socket_addr(first_config))
591 .unwrap();
592 sender
593 .send_to(b"second-1", ipv4_group_socket_addr(second_config))
594 .unwrap();
595 sender
596 .send_to(b"first-2", ipv4_group_socket_addr(first_config))
597 .unwrap();
598 }
599
600 #[test]
601 fn new_context_starts_empty() {
602 let context = Context::new();
603
604 assert_eq!(context.subscription_count(), 0);
605 assert!(context.subscriptions().is_empty());
606 }
607
608 #[test]
609 fn default_uses_the_same_id_sequence_as_new() {
610 let context = Context::default();
611 assert_eq!(context.next_subscription_id, 1);
612 }
613
614 #[test]
615 fn add_subscription_returns_id_and_increases_count() {
616 let mut context = Context::new();
617
618 let id = context
619 .add_subscription(sample_config_on_unused_port())
620 .unwrap();
621
622 assert_eq!(id, SubscriptionId(1));
623 assert_eq!(context.subscription_count(), 1);
624 assert_eq!(context.subscriptions()[0].id(), id);
625 }
626
627 #[test]
628 fn adding_two_subscriptions_generates_different_ids() {
629 let mut context = Context::new();
630
631 let first = context
632 .add_subscription(sample_config_on_unused_port())
633 .unwrap();
634 let second = context
635 .add_subscription(sample_config_on_unused_port())
636 .unwrap();
637
638 assert_ne!(first, second);
639 assert_eq!(first, SubscriptionId(1));
640 assert_eq!(second, SubscriptionId(2));
641 }
642
643 #[test]
644 fn invalid_subscription_is_rejected() {
645 let mut context = Context::new();
646
647 let invalid_config = SubscriptionConfig {
648 group: std::net::IpAddr::V4(Ipv4Addr::new(192, 168, 1, 10)),
649 source: SourceFilter::Any,
650 dst_port: 5000,
651 interface: None,
652 interface_index: None,
653 };
654
655 let result = context.add_subscription(invalid_config);
656
657 assert!(matches!(result, Err(McrxError::InvalidMulticastGroup)));
658 assert_eq!(context.subscription_count(), 0);
659 }
660
661 #[test]
662 fn try_recv_any_returns_packet_from_ready_ipv6_subscription() {
663 if !ipv6_multicast_loopback_available() {
664 return;
665 }
666
667 let mut context = Context::new();
668 let config = sample_config_v6_on_unused_port();
669 let id = context.add_subscription(config.clone()).unwrap();
670 context.join_subscription(id).unwrap();
671
672 let sender = make_multicast_sender_v6(std::net::Ipv6Addr::LOCALHOST);
673 let payload = b"context try_recv_any ipv6 asm";
674 sender
675 .send_to(payload, ipv6_group_socket_addr(&config))
676 .unwrap();
677
678 let deadline = Instant::now() + Duration::from_secs(1);
679 let packet = recv_next_packet(&mut context, deadline);
680
681 assert_eq!(packet.subscription_id, id);
682 assert_eq!(packet.group, config.group);
683 assert_eq!(packet.dst_port, config.dst_port);
684 assert_eq!(&packet.payload[..], payload);
685 }
686
687 #[test]
688 fn try_recv_any_with_metadata_returns_packet_from_ready_ipv6_subscription() {
689 if !ipv6_multicast_loopback_available() {
690 return;
691 }
692
693 let mut context = Context::new();
694 let config = sample_config_v6_on_unused_port();
695 let id = context.add_subscription(config.clone()).unwrap();
696 context.join_subscription(id).unwrap();
697
698 let sender = make_multicast_sender_v6(std::net::Ipv6Addr::LOCALHOST);
699
700 let payload = b"context try_recv_any_with_metadata ipv6 asm";
701 sender
702 .send_to(payload, ipv6_group_socket_addr(&config))
703 .unwrap();
704
705 let deadline = Instant::now() + Duration::from_secs(1);
706 let packet = loop {
707 match context.try_recv_any_with_metadata().unwrap() {
708 Some(packet) => break packet,
709 None if Instant::now() < deadline => {
710 thread::sleep(Duration::from_millis(10));
711 }
712 None => panic!("timed out waiting for IPv6 packet with metadata from context"),
713 }
714 };
715
716 assert_eq!(packet.packet.subscription_id, id);
717 assert_eq!(packet.packet.group, config.group);
718 assert_eq!(packet.packet.dst_port, config.dst_port);
719 assert_eq!(&packet.packet.payload[..], payload);
720 assert_pktinfo_metadata(&packet, config.group);
721 assert_eq!(packet.metadata.configured_interface, config.interface);
722 assert_eq!(
723 packet.metadata.configured_interface_index,
724 config.interface_index
725 );
726 assert_eq!(
727 packet.metadata.socket_local_addr,
728 Some(std::net::SocketAddr::V6(SocketAddrV6::new(
729 Ipv6Addr::UNSPECIFIED,
730 config.dst_port,
731 0,
732 0,
733 )))
734 );
735 }
736
737 #[test]
738 fn remove_existing_subscription_returns_true() {
739 let mut context = Context::new();
740
741 let id = context
742 .add_subscription(sample_config_on_unused_port())
743 .unwrap();
744
745 let removed = context.remove_subscription(id);
746
747 assert!(removed);
748 assert_eq!(context.subscription_count(), 0);
749 }
750
751 #[test]
752 fn remove_missing_subscription_returns_false() {
753 let mut context = Context::new();
754
755 let removed = context.remove_subscription(SubscriptionId(999));
756
757 assert!(!removed);
758 }
759
760 #[test]
761 fn take_subscription_returns_owned_subscription_and_removes_it() {
762 let mut context = Context::new();
763 let config = sample_config_on_unused_port();
764 let id = context.add_subscription(config.clone()).unwrap();
765 context.join_subscription(id).unwrap();
766
767 let subscription = context.take_subscription(id).unwrap();
768
769 assert_eq!(subscription.id(), id);
770 assert_eq!(subscription.config(), &config);
771 assert_eq!(subscription.state(), SubscriptionState::Joined);
772 assert_eq!(context.subscription_count(), 0);
773 assert!(!context.contains_subscription(id));
774 }
775
776 #[test]
777 fn taken_subscription_can_be_split_into_owned_parts() {
778 let mut context = Context::new();
779 let config = sample_config_on_unused_port();
780 let id = context.add_subscription(config.clone()).unwrap();
781 context.join_subscription(id).unwrap();
782
783 let subscription = context.take_subscription(id).unwrap();
784 let parts = subscription.into_parts();
785
786 assert_eq!(parts.id, id);
787 assert_eq!(parts.config, config);
788 assert_eq!(parts.state, SubscriptionState::Joined);
789
790 let local_addr = parts.socket.local_addr().unwrap().as_socket().unwrap();
791 assert_eq!(local_addr.port(), parts.config.dst_port);
792 }
793
794 #[test]
795 fn taken_joined_subscription_can_still_receive_packets() {
796 let mut context = Context::new();
797 let config = sample_config_on_unused_port();
798 let id = context.add_subscription(config.clone()).unwrap();
799 context.join_subscription(id).unwrap();
800
801 let subscription = context.take_subscription(id).unwrap();
802 let sender = make_multicast_sender();
803 let payload = b"context take_subscription handoff";
804
805 sender
806 .send_to(payload, ipv4_group_socket_addr(&config))
807 .unwrap();
808
809 let deadline = Instant::now() + Duration::from_secs(1);
810 let packet = loop {
811 match subscription.try_recv().unwrap() {
812 Some(packet) => break packet,
813 None if Instant::now() < deadline => thread::sleep(Duration::from_millis(10)),
814 None => panic!("timed out waiting for packet after subscription handoff"),
815 }
816 };
817
818 assert_eq!(packet.subscription_id, id);
819 assert_eq!(packet.group, std::net::IpAddr::V4(ipv4_group(&config)));
820 assert_eq!(packet.dst_port, config.dst_port);
821 assert_eq!(&packet.payload[..], payload);
822 }
823
824 #[test]
825 fn three_subscriptions_have_len_3() {
826 let mut context = Context::new();
827
828 context
829 .add_subscription(sample_config_on_unused_port())
830 .unwrap();
831 context
832 .add_subscription(sample_config_on_unused_port())
833 .unwrap();
834 context
835 .add_subscription(sample_config_on_unused_port())
836 .unwrap();
837
838 assert_eq!(context.subscription_count(), 3);
839 }
840
841 #[test]
842 fn duplicate_subscription_is_rejected() {
843 let mut context = Context::new();
844 let config = sample_config_on_unused_port();
845
846 let first = context.add_subscription(config.clone());
847 let second = context.add_subscription(config);
848
849 assert!(first.is_ok());
850 assert!(matches!(second, Err(McrxError::DuplicateSubscription)));
851 assert_eq!(context.subscription_count(), 1);
852 }
853
854 #[test]
855 fn add_subscription_with_socket_returns_id_and_increases_count() {
856 let mut context = Context::new();
857 let config = sample_config_on_unused_port();
858 let socket = make_bound_external_socket(config.dst_port);
859
860 let id = context
861 .add_subscription_with_socket(config, socket)
862 .unwrap();
863
864 assert_eq!(id, SubscriptionId(1));
865 assert_eq!(context.subscription_count(), 1);
866 assert_eq!(context.subscriptions()[0].id(), id);
867 }
868
869 #[test]
870 fn add_subscription_with_socket_rejects_port_mismatch() {
871 let mut context = Context::new();
872 let config = sample_config_on_unused_port();
873 let expected_port = config.dst_port;
874 let socket = make_bound_external_socket(0);
875
876 let result = context.add_subscription_with_socket(config, socket);
877
878 assert!(matches!(
879 result,
880 Err(McrxError::ExistingSocketPortMismatch { expected, .. })
881 if expected == expected_port
882 ));
883 }
884
885 #[test]
886 fn contains_subscription_returns_true_for_existing_id() {
887 let mut context = Context::new();
888 let id = context
889 .add_subscription(sample_config_on_unused_port())
890 .unwrap();
891
892 assert!(context.contains_subscription(id));
893 }
894
895 #[test]
896 fn contains_subscription_returns_false_for_missing_id() {
897 let context = Context::new();
898
899 assert!(!context.contains_subscription(SubscriptionId(999)));
900 }
901
902 #[test]
903 fn get_subscription_returns_matching_subscription() {
904 let mut context = Context::new();
905 let id = context
906 .add_subscription(sample_config_on_unused_port())
907 .unwrap();
908
909 let subscription = context.get_subscription(id);
910
911 assert!(subscription.is_some());
912 assert_eq!(subscription.unwrap().id(), id);
913 }
914
915 #[test]
916 fn get_subscription_returns_none_for_missing_id() {
917 let context = Context::new();
918
919 let subscription = context.get_subscription(SubscriptionId(999));
920
921 assert!(subscription.is_none());
922 }
923
924 #[test]
925 fn get_subscription_mut_returns_matching_subscription() {
926 let mut context = Context::new();
927 let id = context
928 .add_subscription(sample_config_on_unused_port())
929 .unwrap();
930
931 let subscription = context.get_subscription_mut(id);
932
933 assert!(subscription.is_some());
934 assert_eq!(subscription.unwrap().id(), id);
935 }
936
937 #[test]
938 fn get_subscription_mut_returns_none_for_missing_id() {
939 let mut context = Context::new();
940
941 let subscription = context.get_subscription_mut(SubscriptionId(999));
942
943 assert!(subscription.is_none());
944 }
945
946 #[test]
947 fn try_recv_any_returns_none_when_no_packet_is_available() {
948 let mut context = Context::new();
949 let id = context
950 .add_subscription(sample_config_on_unused_port())
951 .unwrap();
952 context.join_subscription(id).unwrap();
953
954 let result = context.try_recv_any().unwrap();
955
956 assert!(result.is_none());
957 }
958
959 #[test]
960 fn try_recv_any_returns_packet_from_ready_subscription() {
961 let mut context = Context::new();
962 let config = sample_config_on_unused_port();
963 let id = context.add_subscription(config.clone()).unwrap();
964 context.join_subscription(id).unwrap();
965
966 let sender = make_multicast_sender();
967
968 let payload = b"context try_recv_any";
969 sender
970 .send_to(payload, ipv4_group_socket_addr(&config))
971 .unwrap();
972
973 let deadline = Instant::now() + Duration::from_secs(1);
974 let packet = recv_next_packet(&mut context, deadline);
975
976 assert_eq!(packet.group, std::net::IpAddr::V4(ipv4_group(&config)));
977 assert_eq!(packet.dst_port, config.dst_port);
978 assert_eq!(&packet.payload[..], payload);
979 }
980
981 #[test]
982 fn try_recv_any_with_metadata_returns_packet_from_ready_subscription() {
983 let mut context = Context::new();
984 let config = sample_config_on_unused_port();
985 let id = context.add_subscription(config.clone()).unwrap();
986 context.join_subscription(id).unwrap();
987
988 let sender = make_multicast_sender();
989
990 let payload = b"context try_recv_any_with_metadata";
991 sender
992 .send_to(payload, ipv4_group_socket_addr(&config))
993 .unwrap();
994
995 let deadline = Instant::now() + Duration::from_secs(1);
996 let packet = loop {
997 match context.try_recv_any_with_metadata().unwrap() {
998 Some(packet) => break packet,
999 None if Instant::now() < deadline => {
1000 thread::sleep(Duration::from_millis(10));
1001 }
1002 None => panic!("timed out waiting for packet with metadata from context"),
1003 }
1004 };
1005
1006 assert_eq!(packet.packet.subscription_id, id);
1007 assert_eq!(
1008 packet.packet.group,
1009 std::net::IpAddr::V4(ipv4_group(&config))
1010 );
1011 assert_eq!(packet.packet.dst_port, config.dst_port);
1012 assert_eq!(&packet.packet.payload[..], payload);
1013 assert_pktinfo_metadata(&packet, std::net::IpAddr::V4(ipv4_group(&config)));
1014 assert_eq!(
1015 packet.metadata.socket_local_addr,
1016 Some(std::net::SocketAddr::V4(SocketAddrV4::new(
1017 Ipv4Addr::UNSPECIFIED,
1018 config.dst_port,
1019 )))
1020 );
1021 }
1022
1023 #[test]
1024 fn try_recv_any_works_with_caller_provided_socket() {
1025 let mut context = Context::new();
1026 let config = sample_config_on_unused_port();
1027 let socket = make_bound_external_socket(config.dst_port);
1028 let id = context
1029 .add_subscription_with_socket(config.clone(), socket)
1030 .unwrap();
1031 context.join_subscription(id).unwrap();
1032
1033 let sender = make_multicast_sender();
1034
1035 let payload = b"context add_subscription_with_socket";
1036 sender
1037 .send_to(payload, ipv4_group_socket_addr(&config))
1038 .unwrap();
1039
1040 let deadline = Instant::now() + Duration::from_secs(1);
1041 let packet = recv_next_packet(&mut context, deadline);
1042
1043 assert_eq!(packet.subscription_id, id);
1044 assert_eq!(packet.group, std::net::IpAddr::V4(ipv4_group(&config)));
1045 assert_eq!(packet.dst_port, config.dst_port);
1046 assert_eq!(&packet.payload[..], payload);
1047 }
1048
1049 #[test]
1050 fn try_recv_any_round_robins_between_ready_subscriptions() {
1051 let mut context = Context::new();
1052 let first_config = sample_config_on_unused_port();
1053 let second_config = sample_config_on_unused_port();
1054
1055 let first_id = context.add_subscription(first_config.clone()).unwrap();
1056 context.join_subscription(first_id).unwrap();
1057 let second_id = context.add_subscription(second_config.clone()).unwrap();
1058 context.join_subscription(second_id).unwrap();
1059
1060 send_round_robin_test_packets(&first_config, &second_config);
1061
1062 let deadline = Instant::now() + Duration::from_secs(1);
1063
1064 let first_packet = recv_next_packet(&mut context, deadline);
1065 let second_packet = recv_next_packet(&mut context, deadline);
1066 let third_packet = recv_next_packet(&mut context, deadline);
1067
1068 assert_eq!(
1069 (first_packet.subscription_id, &first_packet.payload[..]),
1070 (first_id, b"first-1".as_slice())
1071 );
1072
1073 assert_eq!(
1074 (second_packet.subscription_id, &second_packet.payload[..]),
1075 (second_id, b"second-1".as_slice())
1076 );
1077
1078 assert_eq!(
1079 (third_packet.subscription_id, &third_packet.payload[..]),
1080 (first_id, b"first-2".as_slice())
1081 );
1082 }
1083
1084 #[test]
1085 fn try_recv_batch_into_returns_zero_when_no_packet_is_available() {
1086 let mut context = Context::new();
1087 let id = context
1088 .add_subscription(sample_config_on_unused_port())
1089 .unwrap();
1090 context.join_subscription(id).unwrap();
1091
1092 let mut packets = Vec::new();
1093 let received = context.try_recv_batch_into(&mut packets, 8).unwrap();
1094
1095 assert_eq!(received, 0);
1096 assert!(packets.is_empty());
1097 }
1098
1099 #[test]
1100 fn try_recv_batch_into_receives_up_to_max_packets() {
1101 let mut context = Context::new();
1102 let first_config = sample_config_on_unused_port();
1103 let second_config = sample_config_on_unused_port();
1104
1105 let first_id = context.add_subscription(first_config.clone()).unwrap();
1106 context.join_subscription(first_id).unwrap();
1107 let second_id = context.add_subscription(second_config.clone()).unwrap();
1108 context.join_subscription(second_id).unwrap();
1109
1110 send_round_robin_test_packets(&first_config, &second_config);
1111
1112 let deadline = Instant::now() + Duration::from_secs(1);
1113 let mut packets = Vec::new();
1114
1115 while packets.len() < 2 && Instant::now() < deadline {
1116 let before = packets.len();
1117 let received = context.try_recv_batch_into(&mut packets, 2).unwrap();
1118 assert!(received <= 2);
1119 assert_eq!(packets.len(), before + received);
1120
1121 if packets.len() < 2 {
1122 thread::sleep(Duration::from_millis(10));
1123 }
1124 }
1125
1126 assert!(packets.len() >= 2);
1127
1128 assert_eq!(
1129 (packets[0].subscription_id, &packets[0].payload[..]),
1130 (first_id, b"first-1".as_slice())
1131 );
1132
1133 assert_eq!(
1134 (packets[1].subscription_id, &packets[1].payload[..]),
1135 (second_id, b"second-1".as_slice())
1136 );
1137 }
1138
1139 #[test]
1140 fn try_recv_batch_with_metadata_into_receives_up_to_max_packets() {
1141 let mut context = Context::new();
1142 let first_config = sample_config_on_unused_port();
1143 let second_config = sample_config_on_unused_port();
1144
1145 let first_id = context.add_subscription(first_config.clone()).unwrap();
1146 context.join_subscription(first_id).unwrap();
1147 let second_id = context.add_subscription(second_config.clone()).unwrap();
1148 context.join_subscription(second_id).unwrap();
1149
1150 send_round_robin_test_packets(&first_config, &second_config);
1151
1152 let deadline = Instant::now() + Duration::from_secs(1);
1153 let mut packets = Vec::new();
1154
1155 while packets.len() < 2 && Instant::now() < deadline {
1156 let before = packets.len();
1157 let received = context
1158 .try_recv_batch_with_metadata_into(&mut packets, 2)
1159 .unwrap();
1160 assert!(received <= 2);
1161 assert_eq!(packets.len(), before + received);
1162
1163 if packets.len() < 2 {
1164 thread::sleep(Duration::from_millis(10));
1165 }
1166 }
1167
1168 assert!(packets.len() >= 2);
1169
1170 assert_eq!(
1171 (
1172 packets[0].packet.subscription_id,
1173 &packets[0].packet.payload[..]
1174 ),
1175 (first_id, b"first-1".as_slice())
1176 );
1177
1178 assert_eq!(
1179 (
1180 packets[1].packet.subscription_id,
1181 &packets[1].packet.payload[..]
1182 ),
1183 (second_id, b"second-1".as_slice())
1184 );
1185
1186 assert_eq!(
1187 packets[0].metadata.socket_local_addr,
1188 Some(std::net::SocketAddr::V4(SocketAddrV4::new(
1189 Ipv4Addr::UNSPECIFIED,
1190 first_config.dst_port,
1191 )))
1192 );
1193 assert_eq!(
1194 packets[1].metadata.socket_local_addr,
1195 Some(std::net::SocketAddr::V4(SocketAddrV4::new(
1196 Ipv4Addr::UNSPECIFIED,
1197 second_config.dst_port,
1198 )))
1199 );
1200 }
1201
1202 #[test]
1203 fn try_recv_all_into_drains_all_available_packets() {
1204 let mut context = Context::new();
1205 let first_config = sample_config_on_unused_port();
1206 let second_config = sample_config_on_unused_port();
1207
1208 let first_id = context.add_subscription(first_config.clone()).unwrap();
1209 context.join_subscription(first_id).unwrap();
1210 let second_id = context.add_subscription(second_config.clone()).unwrap();
1211 context.join_subscription(second_id).unwrap();
1212
1213 send_round_robin_test_packets(&first_config, &second_config);
1214
1215 let deadline = Instant::now() + Duration::from_secs(1);
1216 let mut packets = Vec::new();
1217
1218 while packets.len() < 3 && Instant::now() < deadline {
1219 context.try_recv_all_into(&mut packets).unwrap();
1220
1221 if packets.len() < 3 {
1222 thread::sleep(Duration::from_millis(10));
1223 }
1224 }
1225
1226 assert_eq!(packets.len(), 3);
1227
1228 assert_eq!(
1229 (packets[0].subscription_id, &packets[0].payload[..]),
1230 (first_id, b"first-1".as_slice())
1231 );
1232
1233 assert_eq!(
1234 (packets[1].subscription_id, &packets[1].payload[..]),
1235 (second_id, b"second-1".as_slice())
1236 );
1237
1238 assert_eq!(
1239 (packets[2].subscription_id, &packets[2].payload[..]),
1240 (first_id, b"first-2".as_slice())
1241 );
1242 }
1243
1244 #[test]
1245 fn try_recv_all_with_metadata_into_drains_all_available_packets() {
1246 let mut context = Context::new();
1247 let first_config = sample_config_on_unused_port();
1248 let second_config = sample_config_on_unused_port();
1249
1250 let first_id = context.add_subscription(first_config.clone()).unwrap();
1251 context.join_subscription(first_id).unwrap();
1252 let second_id = context.add_subscription(second_config.clone()).unwrap();
1253 context.join_subscription(second_id).unwrap();
1254
1255 send_round_robin_test_packets(&first_config, &second_config);
1256
1257 let deadline = Instant::now() + Duration::from_secs(1);
1258 let mut packets = Vec::new();
1259
1260 while packets.len() < 3 && Instant::now() < deadline {
1261 context
1262 .try_recv_all_with_metadata_into(&mut packets)
1263 .unwrap();
1264
1265 if packets.len() < 3 {
1266 thread::sleep(Duration::from_millis(10));
1267 }
1268 }
1269
1270 assert_eq!(packets.len(), 3);
1271
1272 assert_eq!(
1273 (
1274 packets[0].packet.subscription_id,
1275 &packets[0].packet.payload[..]
1276 ),
1277 (first_id, b"first-1".as_slice())
1278 );
1279
1280 assert_eq!(
1281 (
1282 packets[1].packet.subscription_id,
1283 &packets[1].packet.payload[..]
1284 ),
1285 (second_id, b"second-1".as_slice())
1286 );
1287
1288 assert_eq!(
1289 (
1290 packets[2].packet.subscription_id,
1291 &packets[2].packet.payload[..]
1292 ),
1293 (first_id, b"first-2".as_slice())
1294 );
1295 }
1296
1297 #[test]
1298 fn add_subscription_creates_bound_subscription() {
1299 let mut context = Context::new();
1300 let id = context
1301 .add_subscription(sample_config_on_unused_port())
1302 .unwrap();
1303
1304 let subscription = context.get_subscription(id).unwrap();
1305 assert_eq!(subscription.state(), SubscriptionState::Bound);
1306 }
1307
1308 #[test]
1309 fn join_subscription_transitions_bound_to_joined() {
1310 let mut context = Context::new();
1311 let id = context
1312 .add_subscription(sample_config_on_unused_port())
1313 .unwrap();
1314
1315 context.join_subscription(id).unwrap();
1316
1317 let subscription = context.get_subscription(id).unwrap();
1318 assert_eq!(subscription.state(), SubscriptionState::Joined);
1319 }
1320
1321 #[test]
1322 fn leave_subscription_transitions_joined_to_bound() {
1323 let mut context = Context::new();
1324 let id = context
1325 .add_subscription(sample_config_on_unused_port())
1326 .unwrap();
1327
1328 context.join_subscription(id).unwrap();
1329 context.leave_subscription(id).unwrap();
1330
1331 let subscription = context.get_subscription(id).unwrap();
1332 assert_eq!(subscription.state(), SubscriptionState::Bound);
1333 }
1334
1335 #[test]
1336 fn join_subscription_rejects_already_joined_subscription() {
1337 let mut context = Context::new();
1338 let id = context
1339 .add_subscription(sample_config_on_unused_port())
1340 .unwrap();
1341
1342 context.join_subscription(id).unwrap();
1343 let result = context.join_subscription(id);
1344
1345 assert!(matches!(result, Err(McrxError::SubscriptionAlreadyJoined)));
1346 }
1347
1348 #[test]
1349 fn leave_subscription_rejects_not_joined_subscription() {
1350 let mut context = Context::new();
1351 let id = context
1352 .add_subscription(sample_config_on_unused_port())
1353 .unwrap();
1354
1355 let result = context.leave_subscription(id);
1356
1357 assert!(matches!(result, Err(McrxError::SubscriptionNotJoined)));
1358 }
1359}