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#[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 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 pub fn publication_count(&self) -> usize {
108 self.publications.len()
109 }
110
111 pub fn contains_publication(&self, id: PublicationId) -> bool {
113 self.publications
114 .iter()
115 .any(|publication| publication.id() == id)
116 }
117
118 pub fn get_publication(&self, id: PublicationId) -> Option<&Publication> {
120 self.publications
121 .iter()
122 .find(|publication| publication.id() == id)
123 }
124
125 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 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 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 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 pub fn remove_publication(&mut self, id: PublicationId) -> bool {
172 self.take_publication(id).is_some()
173 }
174
175 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 pub fn publications(&self) -> &[Publication] {
187 &self.publications
188 }
189
190 pub fn publications_mut(&mut self) -> &mut [Publication] {
192 &mut self.publications
193 }
194
195 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 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 #[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}