Skip to main content

mctx_core/
context.rs

1#[cfg(feature = "metrics")]
2use crate::metrics::{ContextMetricsSnapshot, MetricsSequence};
3use crate::{MctxError, Publication, PublicationConfig, PublicationId, SendReport};
4use socket2::Socket;
5use std::net::UdpSocket;
6#[cfg(feature = "metrics")]
7use std::sync::atomic::{AtomicU64, Ordering::Relaxed};
8#[cfg(feature = "metrics")]
9use std::time::SystemTime;
10
11#[cfg(feature = "metrics")]
12#[derive(Debug, Default)]
13struct ContextMetricsInner {
14    sequence: MetricsSequence,
15    publications_added: AtomicU64,
16    publications_removed: AtomicU64,
17    total_send_calls: AtomicU64,
18    total_packets_sent: AtomicU64,
19    total_bytes_sent: AtomicU64,
20    total_send_errors: AtomicU64,
21}
22
23/// Small owner for a set of multicast publication sockets.
24#[derive(Debug)]
25pub struct Context {
26    publications: Vec<Publication>,
27    next_id: u64,
28    #[cfg(feature = "metrics")]
29    metrics: ContextMetricsInner,
30}
31
32impl Default for Context {
33    fn default() -> Self {
34        Self::new()
35    }
36}
37
38impl Context {
39    #[cfg(feature = "metrics")]
40    fn record_send_success(&self, bytes_sent: usize) {
41        let _update = self.metrics.sequence.write();
42        self.metrics.total_send_calls.fetch_add(1, Relaxed);
43        self.metrics.total_packets_sent.fetch_add(1, Relaxed);
44        self.metrics
45            .total_bytes_sent
46            .fetch_add(bytes_sent as u64, Relaxed);
47    }
48
49    #[cfg(feature = "metrics")]
50    fn record_send_error(&self) {
51        let _update = self.metrics.sequence.write();
52        self.metrics.total_send_calls.fetch_add(1, Relaxed);
53        self.metrics.total_send_errors.fetch_add(1, Relaxed);
54    }
55
56    fn ensure_publication_config_is_unique(
57        &self,
58        config: &PublicationConfig,
59    ) -> Result<(), MctxError> {
60        if self
61            .publications
62            .iter()
63            .any(|publication| publication.config() == config)
64        {
65            return Err(MctxError::DuplicatePublication);
66        }
67
68        Ok(())
69    }
70
71    fn insert_publication(&mut self, publication: Publication) -> PublicationId {
72        let id = publication.id();
73        self.publications.push(publication);
74
75        #[cfg(feature = "metrics")]
76        {
77            let _update = self.metrics.sequence.write();
78            self.metrics.publications_added.fetch_add(1, Relaxed);
79        }
80
81        id
82    }
83
84    fn finish_publication_removal(&mut self, index: usize) -> Publication {
85        let publication = self.publications.swap_remove(index);
86
87        #[cfg(feature = "metrics")]
88        {
89            let _update = self.metrics.sequence.write();
90            self.metrics.publications_removed.fetch_add(1, Relaxed);
91        }
92
93        publication
94    }
95
96    /// Creates an empty multicast sender context.
97    pub fn new() -> Self {
98        Self {
99            publications: Vec::new(),
100            next_id: 1,
101            #[cfg(feature = "metrics")]
102            metrics: ContextMetricsInner::default(),
103        }
104    }
105
106    /// Returns the number of tracked publications.
107    pub fn publication_count(&self) -> usize {
108        self.publications.len()
109    }
110
111    /// Returns whether a publication ID exists in the context.
112    pub fn contains_publication(&self, id: PublicationId) -> bool {
113        self.publications
114            .iter()
115            .any(|publication| publication.id() == id)
116    }
117
118    /// Returns an immutable reference to one publication.
119    pub fn get_publication(&self, id: PublicationId) -> Option<&Publication> {
120        self.publications
121            .iter()
122            .find(|publication| publication.id() == id)
123    }
124
125    /// Returns a mutable reference to one publication.
126    pub fn get_publication_mut(&mut self, id: PublicationId) -> Option<&mut Publication> {
127        self.publications
128            .iter_mut()
129            .find(|publication| publication.id() == id)
130    }
131
132    /// Creates a new publication socket from configuration and stores it.
133    pub fn add_publication(
134        &mut self,
135        config: PublicationConfig,
136    ) -> Result<PublicationId, MctxError> {
137        self.ensure_publication_config_is_unique(&config)?;
138
139        let id = self.next_publication_id();
140        let publication = Publication::new(id, config)?;
141        Ok(self.insert_publication(publication))
142    }
143
144    /// Stores an existing socket as a publication after configuring it.
145    pub fn add_publication_with_socket(
146        &mut self,
147        config: PublicationConfig,
148        socket: Socket,
149    ) -> Result<PublicationId, MctxError> {
150        self.ensure_publication_config_is_unique(&config)?;
151
152        let id = self.next_publication_id();
153        let publication = Publication::new_with_socket(id, config, socket)?;
154        Ok(self.insert_publication(publication))
155    }
156
157    /// Stores an existing standard-library UDP socket as a publication after configuring it.
158    pub fn add_publication_with_udp_socket(
159        &mut self,
160        config: PublicationConfig,
161        socket: UdpSocket,
162    ) -> Result<PublicationId, MctxError> {
163        self.ensure_publication_config_is_unique(&config)?;
164
165        let id = self.next_publication_id();
166        let publication = Publication::new_with_udp_socket(id, config, socket)?;
167        Ok(self.insert_publication(publication))
168    }
169
170    /// Removes one publication and drops its socket.
171    pub fn remove_publication(&mut self, id: PublicationId) -> bool {
172        self.take_publication(id).is_some()
173    }
174
175    /// Extracts one publication from the context.
176    pub fn take_publication(&mut self, id: PublicationId) -> Option<Publication> {
177        let index = self
178            .publications
179            .iter()
180            .position(|publication| publication.id() == id)?;
181
182        Some(self.finish_publication_removal(index))
183    }
184
185    /// Returns all tracked publications.
186    pub fn publications(&self) -> &[Publication] {
187        &self.publications
188    }
189
190    /// Returns all tracked publications mutably.
191    pub fn publications_mut(&mut self) -> &mut [Publication] {
192        &mut self.publications
193    }
194
195    /// Sends one payload through the selected publication.
196    pub fn send(&self, id: PublicationId, payload: &[u8]) -> Result<SendReport, MctxError> {
197        let publication = self
198            .get_publication(id)
199            .ok_or(MctxError::PublicationNotFound)?;
200
201        match publication.send(payload) {
202            Ok(report) => {
203                #[cfg(feature = "metrics")]
204                self.record_send_success(report.bytes_sent);
205
206                Ok(report)
207            }
208            Err(error) => {
209                #[cfg(feature = "metrics")]
210                self.record_send_error();
211
212                Err(error)
213            }
214        }
215    }
216
217    /// Sends the same payload through every publication and pushes reports into `out`.
218    ///
219    /// If one publication fails, reports already written into `out` are preserved.
220    pub fn send_all(&self, payload: &[u8], out: &mut Vec<SendReport>) -> Result<usize, MctxError> {
221        let before = out.len();
222        out.reserve(self.publications.len());
223
224        for publication in &self.publications {
225            match publication.send(payload) {
226                Ok(report) => {
227                    #[cfg(feature = "metrics")]
228                    self.record_send_success(report.bytes_sent);
229
230                    out.push(report);
231                }
232                Err(error) => {
233                    #[cfg(feature = "metrics")]
234                    self.record_send_error();
235
236                    return Err(error);
237                }
238            }
239        }
240
241        Ok(out.len() - before)
242    }
243
244    /// Returns a snapshot of the context's current metrics.
245    ///
246    /// Counter fields such as `total_packets_sent` are cumulative for the
247    /// lifetime of the context for send activity issued through `Context`
248    /// methods. They are not recomputed from the currently active publications,
249    /// and they do not decrease when a publication is removed.
250    #[cfg(feature = "metrics")]
251    pub fn metrics_snapshot(&self) -> ContextMetricsSnapshot {
252        let (
253            publications_added,
254            publications_removed,
255            total_send_calls,
256            total_packets_sent,
257            total_bytes_sent,
258            total_send_errors,
259        ) = self.metrics.sequence.read_consistent(|| {
260            (
261                self.metrics.publications_added.load(Relaxed),
262                self.metrics.publications_removed.load(Relaxed),
263                self.metrics.total_send_calls.load(Relaxed),
264                self.metrics.total_packets_sent.load(Relaxed),
265                self.metrics.total_bytes_sent.load(Relaxed),
266                self.metrics.total_send_errors.load(Relaxed),
267            )
268        });
269
270        ContextMetricsSnapshot {
271            publications_added,
272            publications_removed,
273            active_publications: self.publications.len(),
274            total_send_calls,
275            total_packets_sent,
276            total_bytes_sent,
277            total_send_errors,
278            captured_at: SystemTime::now(),
279        }
280    }
281
282    fn next_publication_id(&mut self) -> PublicationId {
283        let id = PublicationId(self.next_id);
284        self.next_id += 1;
285        id
286    }
287}
288
289#[cfg(test)]
290mod tests {
291    use super::*;
292    #[cfg(feature = "metrics")]
293    use crate::metrics::ContextMetricsSampler;
294    use crate::test_support::{
295        TEST_GROUP, multicast_test_result_or_skip, recv_payload, test_multicast_receiver,
296    };
297    use std::net::Ipv4Addr;
298
299    #[test]
300    fn context_send_reaches_a_local_receiver() {
301        let (receiver, port) = test_multicast_receiver();
302        let mut context = Context::new();
303        let config = PublicationConfig::new(TEST_GROUP, port);
304        let id = context.add_publication(config).unwrap();
305
306        let Some(report) = multicast_test_result_or_skip(context.send(id, b"context hello")) else {
307            return;
308        };
309        let payload = recv_payload(&receiver);
310
311        assert_eq!(report.bytes_sent, b"context hello".len());
312        assert_eq!(payload, b"context hello");
313    }
314
315    #[test]
316    fn duplicate_publications_are_rejected() {
317        let mut context = Context::new();
318        let config = PublicationConfig::new(Ipv4Addr::new(239, 1, 2, 3), 5000);
319
320        context.add_publication(config.clone()).unwrap();
321        let result = context.add_publication(config);
322
323        assert!(matches!(result, Err(MctxError::DuplicatePublication)));
324    }
325
326    #[cfg(feature = "metrics")]
327    #[test]
328    fn context_metrics_track_successful_sends() {
329        let (_receiver, port) = test_multicast_receiver();
330        let mut context = Context::new();
331        let id = context
332            .add_publication(PublicationConfig::new(TEST_GROUP, port))
333            .unwrap();
334        let mut sampler = ContextMetricsSampler::new(&context);
335
336        assert!(sampler.sample().is_none());
337        if multicast_test_result_or_skip(context.send(id, b"metrics")).is_none() {
338            return;
339        }
340
341        let snapshot = context.metrics_snapshot();
342        let delta = sampler.sample().unwrap();
343
344        assert_eq!(snapshot.publications_added, 1);
345        assert_eq!(snapshot.publications_removed, 0);
346        assert_eq!(snapshot.active_publications, 1);
347        assert_eq!(snapshot.total_send_calls, 1);
348        assert_eq!(snapshot.total_packets_sent, 1);
349        assert_eq!(snapshot.total_bytes_sent, b"metrics".len() as u64);
350        assert_eq!(snapshot.total_send_errors, 0);
351        assert_eq!(delta.publications_added, 0);
352        assert_eq!(delta.publications_removed, 0);
353        assert_eq!(delta.send_calls, 1);
354        assert_eq!(delta.packets_sent, 1);
355        assert_eq!(delta.bytes_sent, b"metrics".len() as u64);
356        assert_eq!(delta.send_errors, 0);
357    }
358
359    #[cfg(feature = "metrics")]
360    #[test]
361    fn context_metrics_totals_survive_publication_removal() {
362        let (_receiver, port) = test_multicast_receiver();
363        let mut context = Context::new();
364        let id = context
365            .add_publication(PublicationConfig::new(TEST_GROUP, port))
366            .unwrap();
367
368        if multicast_test_result_or_skip(context.send(id, b"lifetime")).is_none() {
369            return;
370        }
371        let before_removal = context.metrics_snapshot();
372        assert!(context.remove_publication(id));
373
374        let after_removal = context.metrics_snapshot();
375
376        assert_eq!(before_removal.total_packets_sent, 1);
377        assert_eq!(before_removal.total_bytes_sent, b"lifetime".len() as u64);
378        assert_eq!(after_removal.total_packets_sent, 1);
379        assert_eq!(after_removal.total_bytes_sent, b"lifetime".len() as u64);
380        assert_eq!(after_removal.active_publications, 0);
381        assert_eq!(after_removal.publications_removed, 1);
382    }
383
384    #[cfg(feature = "metrics")]
385    #[test]
386    fn context_is_sync_with_metrics_enabled() {
387        fn assert_sync<T: Sync>() {}
388
389        assert_sync::<Context>();
390    }
391}