1#[cfg(feature = "metrics")]
2use crate::metrics::ContextMetricsSnapshot;
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 publications_added: AtomicU64,
15 publications_removed: AtomicU64,
16 total_send_calls: AtomicU64,
17 total_packets_sent: AtomicU64,
18 total_bytes_sent: AtomicU64,
19 total_send_errors: AtomicU64,
20}
21
22#[derive(Debug)]
24pub struct Context {
25 publications: Vec<Publication>,
26 next_id: u64,
27 #[cfg(feature = "metrics")]
28 metrics: ContextMetricsInner,
29}
30
31impl Default for Context {
32 fn default() -> Self {
33 Self::new()
34 }
35}
36
37impl Context {
38 #[cfg(feature = "metrics")]
39 fn record_send_success(&self, bytes_sent: usize) {
40 self.metrics.total_send_calls.fetch_add(1, Relaxed);
41 self.metrics.total_packets_sent.fetch_add(1, Relaxed);
42 self.metrics
43 .total_bytes_sent
44 .fetch_add(bytes_sent as u64, Relaxed);
45 }
46
47 #[cfg(feature = "metrics")]
48 fn record_send_error(&self) {
49 self.metrics.total_send_calls.fetch_add(1, Relaxed);
50 self.metrics.total_send_errors.fetch_add(1, Relaxed);
51 }
52
53 fn ensure_publication_config_is_unique(
54 &self,
55 config: &PublicationConfig,
56 ) -> Result<(), MctxError> {
57 if self
58 .publications
59 .iter()
60 .any(|publication| publication.config() == config)
61 {
62 return Err(MctxError::DuplicatePublication);
63 }
64
65 Ok(())
66 }
67
68 fn insert_publication(&mut self, publication: Publication) -> PublicationId {
69 let id = publication.id();
70 self.publications.push(publication);
71
72 #[cfg(feature = "metrics")]
73 self.metrics.publications_added.fetch_add(1, Relaxed);
74
75 id
76 }
77
78 fn finish_publication_removal(&mut self, index: usize) -> Publication {
79 let publication = self.publications.swap_remove(index);
80
81 #[cfg(feature = "metrics")]
82 self.metrics.publications_removed.fetch_add(1, Relaxed);
83
84 publication
85 }
86
87 pub fn new() -> Self {
89 Self {
90 publications: Vec::new(),
91 next_id: 1,
92 #[cfg(feature = "metrics")]
93 metrics: ContextMetricsInner::default(),
94 }
95 }
96
97 pub fn publication_count(&self) -> usize {
99 self.publications.len()
100 }
101
102 pub fn contains_publication(&self, id: PublicationId) -> bool {
104 self.publications
105 .iter()
106 .any(|publication| publication.id() == id)
107 }
108
109 pub fn get_publication(&self, id: PublicationId) -> Option<&Publication> {
111 self.publications
112 .iter()
113 .find(|publication| publication.id() == id)
114 }
115
116 pub fn get_publication_mut(&mut self, id: PublicationId) -> Option<&mut Publication> {
118 self.publications
119 .iter_mut()
120 .find(|publication| publication.id() == id)
121 }
122
123 pub fn add_publication(
125 &mut self,
126 config: PublicationConfig,
127 ) -> Result<PublicationId, MctxError> {
128 self.ensure_publication_config_is_unique(&config)?;
129
130 let id = self.next_publication_id();
131 let publication = Publication::new(id, config)?;
132 Ok(self.insert_publication(publication))
133 }
134
135 pub fn add_publication_with_socket(
137 &mut self,
138 config: PublicationConfig,
139 socket: Socket,
140 ) -> Result<PublicationId, MctxError> {
141 self.ensure_publication_config_is_unique(&config)?;
142
143 let id = self.next_publication_id();
144 let publication = Publication::new_with_socket(id, config, socket)?;
145 Ok(self.insert_publication(publication))
146 }
147
148 pub fn add_publication_with_udp_socket(
150 &mut self,
151 config: PublicationConfig,
152 socket: UdpSocket,
153 ) -> Result<PublicationId, MctxError> {
154 self.ensure_publication_config_is_unique(&config)?;
155
156 let id = self.next_publication_id();
157 let publication = Publication::new_with_udp_socket(id, config, socket)?;
158 Ok(self.insert_publication(publication))
159 }
160
161 pub fn remove_publication(&mut self, id: PublicationId) -> bool {
163 self.take_publication(id).is_some()
164 }
165
166 pub fn take_publication(&mut self, id: PublicationId) -> Option<Publication> {
168 let index = self
169 .publications
170 .iter()
171 .position(|publication| publication.id() == id)?;
172
173 Some(self.finish_publication_removal(index))
174 }
175
176 pub fn publications(&self) -> &[Publication] {
178 &self.publications
179 }
180
181 pub fn publications_mut(&mut self) -> &mut [Publication] {
183 &mut self.publications
184 }
185
186 pub fn send(&self, id: PublicationId, payload: &[u8]) -> Result<SendReport, MctxError> {
188 let publication = self
189 .get_publication(id)
190 .ok_or(MctxError::PublicationNotFound)?;
191
192 match publication.send(payload) {
193 Ok(report) => {
194 #[cfg(feature = "metrics")]
195 self.record_send_success(report.bytes_sent);
196
197 Ok(report)
198 }
199 Err(error) => {
200 #[cfg(feature = "metrics")]
201 self.record_send_error();
202
203 Err(error)
204 }
205 }
206 }
207
208 pub fn send_all(&self, payload: &[u8], out: &mut Vec<SendReport>) -> Result<usize, MctxError> {
212 let before = out.len();
213 out.reserve(self.publications.len());
214
215 for publication in &self.publications {
216 match publication.send(payload) {
217 Ok(report) => {
218 #[cfg(feature = "metrics")]
219 self.record_send_success(report.bytes_sent);
220
221 out.push(report);
222 }
223 Err(error) => {
224 #[cfg(feature = "metrics")]
225 self.record_send_error();
226
227 return Err(error);
228 }
229 }
230 }
231
232 Ok(out.len() - before)
233 }
234
235 #[cfg(feature = "metrics")]
242 pub fn metrics_snapshot(&self) -> ContextMetricsSnapshot {
243 ContextMetricsSnapshot {
244 publications_added: self.metrics.publications_added.load(Relaxed),
245 publications_removed: self.metrics.publications_removed.load(Relaxed),
246 active_publications: self.publications.len(),
247 total_send_calls: self.metrics.total_send_calls.load(Relaxed),
248 total_packets_sent: self.metrics.total_packets_sent.load(Relaxed),
249 total_bytes_sent: self.metrics.total_bytes_sent.load(Relaxed),
250 total_send_errors: self.metrics.total_send_errors.load(Relaxed),
251 captured_at: SystemTime::now(),
252 }
253 }
254
255 fn next_publication_id(&mut self) -> PublicationId {
256 let id = PublicationId(self.next_id);
257 self.next_id += 1;
258 id
259 }
260}
261
262#[cfg(test)]
263mod tests {
264 use super::*;
265 #[cfg(feature = "metrics")]
266 use crate::metrics::ContextMetricsSampler;
267 use crate::test_support::{TEST_GROUP, recv_payload, test_multicast_receiver};
268 use std::net::Ipv4Addr;
269
270 #[test]
271 fn context_send_reaches_a_local_receiver() {
272 let (receiver, port) = test_multicast_receiver();
273 let mut context = Context::new();
274 let config = PublicationConfig::new(TEST_GROUP, port);
275 let id = context.add_publication(config).unwrap();
276
277 let report = context.send(id, b"context hello").unwrap();
278 let payload = recv_payload(&receiver);
279
280 assert_eq!(report.bytes_sent, b"context hello".len());
281 assert_eq!(payload, b"context hello");
282 }
283
284 #[test]
285 fn duplicate_publications_are_rejected() {
286 let mut context = Context::new();
287 let config = PublicationConfig::new(Ipv4Addr::new(239, 1, 2, 3), 5000);
288
289 context.add_publication(config.clone()).unwrap();
290 let result = context.add_publication(config);
291
292 assert!(matches!(result, Err(MctxError::DuplicatePublication)));
293 }
294
295 #[cfg(feature = "metrics")]
296 #[test]
297 fn context_metrics_track_successful_sends() {
298 let (_receiver, port) = test_multicast_receiver();
299 let mut context = Context::new();
300 let id = context
301 .add_publication(PublicationConfig::new(TEST_GROUP, port))
302 .unwrap();
303 let mut sampler = ContextMetricsSampler::new(&context);
304
305 assert!(sampler.sample().is_none());
306 context.send(id, b"metrics").unwrap();
307
308 let snapshot = context.metrics_snapshot();
309 let delta = sampler.sample().unwrap();
310
311 assert_eq!(snapshot.publications_added, 1);
312 assert_eq!(snapshot.publications_removed, 0);
313 assert_eq!(snapshot.active_publications, 1);
314 assert_eq!(snapshot.total_send_calls, 1);
315 assert_eq!(snapshot.total_packets_sent, 1);
316 assert_eq!(snapshot.total_bytes_sent, b"metrics".len() as u64);
317 assert_eq!(snapshot.total_send_errors, 0);
318 assert_eq!(delta.publications_added, 0);
319 assert_eq!(delta.publications_removed, 0);
320 assert_eq!(delta.send_calls, 1);
321 assert_eq!(delta.packets_sent, 1);
322 assert_eq!(delta.bytes_sent, b"metrics".len() as u64);
323 assert_eq!(delta.send_errors, 0);
324 }
325
326 #[cfg(feature = "metrics")]
327 #[test]
328 fn context_metrics_totals_survive_publication_removal() {
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
335 context.send(id, b"lifetime").unwrap();
336 let before_removal = context.metrics_snapshot();
337 assert!(context.remove_publication(id));
338
339 let after_removal = context.metrics_snapshot();
340
341 assert_eq!(before_removal.total_packets_sent, 1);
342 assert_eq!(before_removal.total_bytes_sent, b"lifetime".len() as u64);
343 assert_eq!(after_removal.total_packets_sent, 1);
344 assert_eq!(after_removal.total_bytes_sent, b"lifetime".len() as u64);
345 assert_eq!(after_removal.active_publications, 0);
346 assert_eq!(after_removal.publications_removed, 1);
347 }
348
349 #[cfg(feature = "metrics")]
350 #[test]
351 fn context_is_sync_with_metrics_enabled() {
352 fn assert_sync<T: Sync>() {}
353
354 assert_sync::<Context>();
355 }
356}