Skip to main content

mcrx_core/
context.rs

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/// Owns and manages the set of active subscriptions.
34#[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    /// Creates an empty context with no subscriptions.
140    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    /// Returns the number of active subscriptions currently stored in the context.
151    pub fn subscription_count(&self) -> usize {
152        self.subscriptions.len()
153    }
154
155    /// Returns a snapshot of the context's current metrics.
156    ///
157    /// Counter fields such as `total_packets_received` are cumulative for the
158    /// lifetime of the context and are not reduced when subscriptions are
159    /// removed.
160    #[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    /// Returns true if a subscription with the given ID exists.
188    pub fn contains_subscription(&self, id: SubscriptionId) -> bool {
189        self.subscriptions
190            .iter()
191            .any(|subscription| subscription.id() == id)
192    }
193
194    /// Returns a read-only reference to the subscription with the given ID, if it exists.
195    pub fn get_subscription(&self, id: SubscriptionId) -> Option<&Subscription> {
196        self.subscriptions
197            .iter()
198            .find(|subscription| subscription.id() == id)
199    }
200
201    /// Returns a mutable reference to the subscription with the given ID, if it exists.
202    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    /// Adds a new subscription to the context.
209    ///
210    /// The configuration is validated before insertion. If an identical subscription
211    /// already exists, an error is returned instead of creating a duplicate.
212    ///
213    /// This function creates and binds the underlying socket, but does not join the
214    /// multicast group yet. Call `join_subscription()` to activate multicast reception.
215    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    /// Adds a new subscription using a caller-provided socket.
227    ///
228    /// The socket must already be bound to the destination port from `config`.
229    /// This method preserves the existing lifecycle model: the context will still
230    /// perform multicast join/leave operations later via `join_subscription()` and
231    /// `leave_subscription()`.
232    ///
233    /// The supplied socket is switched to non-blocking mode so the receive APIs keep
234    /// their usual non-blocking behavior.
235    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    /// Removes the subscription with the given ID.
249    ///
250    /// Returns true if a subscription was removed and false if no matching
251    /// subscription was found.
252    ///
253    /// This uses `swap_remove`, so subscription order is not preserved.
254    pub fn remove_subscription(&mut self, id: SubscriptionId) -> bool {
255        self.take_subscription(id).is_some()
256    }
257
258    /// Removes the subscription with the given ID and returns it to the caller.
259    ///
260    /// This preserves the current socket ownership and lifecycle state, which is
261    /// useful when moving a subscription into an external event loop or runtime.
262    ///
263    /// This uses `swap_remove`, so subscription order is not preserved.
264    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    /// Joins the multicast group for the given subscription.
274    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    /// Leaves the multicast group for the given subscription while keeping the socket bound.
349    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    /// Returns a read-only slice of all subscriptions currently stored in the context.
370    pub fn subscriptions(&self) -> &[Subscription] {
371        &self.subscriptions
372    }
373
374    /// Returns a mutable slice of all subscriptions currently stored in the context.
375    pub fn subscriptions_mut(&mut self) -> &mut [Subscription] {
376        &mut self.subscriptions
377    }
378
379    /// Attempts to receive a single packet from any joined subscription without blocking.
380    ///
381    /// Subscriptions are scanned using round-robin style fairness so that repeated
382    /// calls do not always favor the first subscription.
383    ///
384    /// Returns the first available packet, if any joined subscription currently has
385    /// one ready to be read.
386    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    /// Attempts to receive a single packet with richer receive metadata from any
391    /// joined subscription without blocking.
392    ///
393    /// This uses the same round-robin fairness logic as `try_recv_any()`.
394    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    /// Attempts to receive up to `max_packets` packets from any subscriptions without blocking.
401    ///
402    /// This method repeatedly calls `try_recv_any()` using the same round-robin fairness logic
403    /// and pushes received packets into the provided `out` vector.
404    ///
405    /// Returns the number of packets that were added to `out`.
406    ///
407    /// Behavior:
408    /// - Stops early if no more packets are available
409    /// - Does not block or wait for new packets
410    /// - Preserves fairness across subscriptions
411    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    /// Attempts to receive up to `max_packets` packets with richer receive
446    /// metadata from any subscriptions without blocking.
447    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    /// Attempts to receive currently available packets without blocking.
482    ///
483    /// This is a convenience wrapper around `try_recv_batch_into` that drains at
484    /// most 4096 packets per call. The limit prevents a continuously busy socket
485    /// from monopolizing the caller or growing `out` without bound. Callers that
486    /// need a different budget should use `try_recv_batch_into` directly.
487    ///
488    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    /// Attempts to receive up to 4096 currently available packets with richer
496    /// receive metadata without blocking.
497    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}