1use std::fmt;
18use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
19use std::sync::Arc;
20
21use chrono::{DateTime, Utc};
22use tokio::sync::{mpsc, watch};
23
24use crate::adapter::{EndReason, TransferAttemptId, TransferStatus, TransferTarget};
25use crate::connection::Transport;
26use crate::ids::ConnectionId;
27use crate::DataMessage;
28
29#[derive(Clone, Copy, Debug, Eq, PartialEq)]
31#[non_exhaustive]
32pub enum OperationalEventStreamHealth {
33 NotInstalled,
36 Healthy,
38 Degraded,
41}
42
43#[derive(Clone, Copy, Debug)]
44pub(crate) enum OperationalEventStreamFailure {
45 ReceiverLost,
46 DeliveryCancelled,
47 SequenceExhausted,
48 SendFailed,
49}
50
51impl OperationalEventStreamFailure {
52 fn metric_label(self) -> &'static str {
53 match self {
54 Self::ReceiverLost => "receiver_lost",
55 Self::DeliveryCancelled => "delivery_cancelled",
56 Self::SequenceExhausted => "sequence_exhausted",
57 Self::SendFailed => "send_failed",
58 }
59 }
60}
61
62#[derive(Clone)]
70pub struct OperationalEventStreamHealthSubscription {
71 updates: watch::Receiver<OperationalEventStreamHealth>,
72 receiver_closed: mpsc::Sender<OperationalEvent>,
73 health: Arc<OperationalEventStreamHealthState>,
74}
75
76impl OperationalEventStreamHealthSubscription {
77 pub fn current(&self) -> OperationalEventStreamHealth {
79 if self.receiver_closed.is_closed() {
80 self.health
81 .mark_degraded(OperationalEventStreamFailure::ReceiverLost);
82 }
83 self.health.current()
84 }
85
86 pub async fn changed(&mut self) -> OperationalEventStreamHealth {
92 let current = self.current();
93 if current == OperationalEventStreamHealth::Degraded {
94 self.updates.borrow_and_update();
95 return current;
96 }
97 tokio::select! {
98 changed = self.updates.changed() => {
99 if changed.is_err() {
100 self.health.mark_degraded(
105 OperationalEventStreamFailure::DeliveryCancelled,
106 );
107 }
108 }
109 () = self.receiver_closed.closed() => {
110 self.health.mark_degraded(
111 OperationalEventStreamFailure::ReceiverLost,
112 );
113 }
114 }
115 let current = self.health.current();
116 self.updates.borrow_and_update();
117 current
118 }
119}
120
121#[derive(Clone, Copy, Debug, Eq, PartialEq)]
124#[non_exhaustive]
125pub enum OperationalEndReason {
126 Normal,
127 Cancelled,
128 Failed,
129 Timeout,
130 BridgeTorn,
131}
132
133impl From<&EndReason> for OperationalEndReason {
134 fn from(reason: &EndReason) -> Self {
135 match reason {
136 EndReason::Normal => Self::Normal,
137 EndReason::Cancelled => Self::Cancelled,
138 EndReason::Failed { .. } => Self::Failed,
139 EndReason::Timeout => Self::Timeout,
140 EndReason::BridgeTorn => Self::BridgeTorn,
141 }
142 }
143}
144
145#[derive(Clone, Copy, Debug, Eq, PartialEq)]
148#[non_exhaustive]
149pub enum OperationalFailureReason {
150 AdapterReported,
151 CoreReported,
152}
153
154#[derive(Clone, Debug, Eq, PartialEq)]
157#[non_exhaustive]
158pub enum OperationalTransferTarget {
159 Uri,
160 Connection(ConnectionId),
161 Session(crate::ids::SessionId),
162}
163
164impl From<&TransferTarget> for OperationalTransferTarget {
165 fn from(target: &TransferTarget) -> Self {
166 match target {
167 TransferTarget::Uri(_) => Self::Uri,
168 TransferTarget::Connection(connection_id) => Self::Connection(connection_id.clone()),
169 TransferTarget::Session(session_id) => Self::Session(session_id.clone()),
170 }
171 }
172}
173
174#[derive(Clone, Copy, Debug, Eq, PartialEq)]
175#[non_exhaustive]
176pub enum OperationalTransferOutcome {
177 Submitted,
179 Succeeded,
180 Failed,
181}
182
183#[derive(Clone, Eq, PartialEq)]
189#[non_exhaustive]
190pub enum OperationalEventKind {
191 Connected,
192 Progress {
198 status_code: u16,
199 early_media: bool,
200 },
201 MediaActivity {
206 generation: u64,
207 },
208 Ended {
209 reason: OperationalEndReason,
210 },
211 Failed {
212 reason: OperationalFailureReason,
213 },
214 Dtmf {
215 digits: String,
216 duration_ms: u32,
217 },
218 DataMessage {
219 message: DataMessage,
220 },
221 Transfer {
222 attempt_id: Option<TransferAttemptId>,
225 target: OperationalTransferTarget,
226 outcome: OperationalTransferOutcome,
227 },
228 TransferStatus {
230 attempt_id: Option<TransferAttemptId>,
231 status: TransferStatus,
232 },
233}
234
235impl fmt::Debug for OperationalEventKind {
236 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
237 match self {
238 Self::Connected => formatter.write_str("Connected"),
239 Self::Progress {
240 status_code,
241 early_media,
242 } => formatter
243 .debug_struct("Progress")
244 .field("status_code", status_code)
245 .field("early_media", early_media)
246 .finish(),
247 Self::MediaActivity { generation } => formatter
248 .debug_struct("MediaActivity")
249 .field("generation", generation)
250 .finish(),
251 Self::Ended { reason } => formatter
252 .debug_struct("Ended")
253 .field("reason", reason)
254 .finish(),
255 Self::Failed { reason } => formatter
256 .debug_struct("Failed")
257 .field("reason", reason)
258 .finish(),
259 Self::Dtmf { duration_ms, .. } => formatter
260 .debug_struct("Dtmf")
261 .field("digits", &"[redacted]")
262 .field("duration_ms", duration_ms)
263 .finish(),
264 Self::DataMessage { message } => formatter
265 .debug_struct("DataMessage")
266 .field("label", &"[redacted]")
267 .field("content_type", &"[redacted]")
268 .field("body_bytes", &message.bytes.len())
269 .field("message_id", &"[redacted]")
270 .field("reliability", &message.reliability)
271 .finish(),
272 Self::Transfer {
273 attempt_id,
274 target,
275 outcome,
276 } => formatter
277 .debug_struct("Transfer")
278 .field("attempt_id_present", &attempt_id.is_some())
279 .field("target", &RedactedTransferTarget(target))
280 .field("outcome", outcome)
281 .finish(),
282 Self::TransferStatus { attempt_id, status } => formatter
283 .debug_struct("TransferStatus")
284 .field("attempt_id_present", &attempt_id.is_some())
285 .field("status", status)
286 .finish(),
287 }
288 }
289}
290
291struct RedactedTransferTarget<'a>(&'a OperationalTransferTarget);
292
293impl fmt::Debug for RedactedTransferTarget<'_> {
294 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
295 match self.0 {
296 OperationalTransferTarget::Uri => formatter.write_str("Uri([redacted])"),
297 OperationalTransferTarget::Connection(_) => {
298 formatter.write_str("Connection([redacted])")
299 }
300 OperationalTransferTarget::Session(_) => formatter.write_str("Session([redacted])"),
301 }
302 }
303}
304
305#[derive(Clone, Eq, PartialEq)]
307#[non_exhaustive]
308pub struct OperationalEvent {
309 pub sequence: u64,
310 pub connection_id: ConnectionId,
311 pub transport: Transport,
312 pub at: DateTime<Utc>,
313 pub kind: OperationalEventKind,
314}
315
316impl fmt::Debug for OperationalEvent {
317 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
318 formatter
319 .debug_struct("OperationalEvent")
320 .field("sequence", &self.sequence)
321 .field("connection_id", &self.connection_id)
322 .field("transport", &self.transport)
323 .field("at", &self.at)
324 .field("kind", &self.kind)
325 .finish()
326 }
327}
328
329pub(crate) struct OperationalEventStream {
331 sender: mpsc::Sender<OperationalEvent>,
332 next_sequence: AtomicU64,
333 health: Arc<OperationalEventStreamHealthState>,
334}
335
336struct OperationalEventStreamHealthState {
337 degraded: AtomicBool,
338 updates: watch::Sender<OperationalEventStreamHealth>,
339}
340
341pub(crate) struct OperationalSendGuard<'a> {
342 stream: &'a OperationalEventStream,
343 armed: bool,
344}
345
346impl OperationalSendGuard<'_> {
347 pub(crate) fn disarm(&mut self) {
348 self.armed = false;
349 }
350}
351
352impl Drop for OperationalSendGuard<'_> {
353 fn drop(&mut self) {
354 if self.armed {
355 self.stream
356 .mark_degraded(OperationalEventStreamFailure::DeliveryCancelled);
357 }
358 }
359}
360
361impl OperationalEventStreamHealthState {
362 fn current(&self) -> OperationalEventStreamHealth {
363 if self.degraded.load(Ordering::Acquire) {
364 OperationalEventStreamHealth::Degraded
365 } else {
366 OperationalEventStreamHealth::Healthy
367 }
368 }
369
370 fn mark_degraded(&self, failure: OperationalEventStreamFailure) {
371 if !self.degraded.swap(true, Ordering::AcqRel) {
372 self.updates
373 .send_replace(OperationalEventStreamHealth::Degraded);
374 metrics::counter!(
375 "rvoip_core_operational_event_stream_failures_total",
376 "reason" => failure.metric_label()
377 )
378 .increment(1);
379 tracing::error!(
380 reason = failure.metric_label(),
381 "authoritative operational event stream degraded; failing closed"
382 );
383 }
384 }
385}
386
387impl OperationalEventStream {
388 pub(crate) fn new(capacity: usize) -> (Self, mpsc::Receiver<OperationalEvent>) {
389 let (sender, receiver) = mpsc::channel(capacity);
390 let (health_updates, _initial_health) =
391 watch::channel(OperationalEventStreamHealth::Healthy);
392 (
393 Self {
394 sender,
395 next_sequence: AtomicU64::new(1),
396 health: Arc::new(OperationalEventStreamHealthState {
397 degraded: AtomicBool::new(false),
398 updates: health_updates,
399 }),
400 },
401 receiver,
402 )
403 }
404
405 pub(crate) fn health(&self) -> OperationalEventStreamHealth {
406 if self.sender.is_closed() {
407 self.mark_degraded(OperationalEventStreamFailure::ReceiverLost);
408 }
409 self.health.current()
410 }
411
412 pub(crate) fn subscribe_health(&self) -> OperationalEventStreamHealthSubscription {
413 let _ = self.health();
417 OperationalEventStreamHealthSubscription {
418 updates: self.health.updates.subscribe(),
419 receiver_closed: self.sender.clone(),
420 health: Arc::clone(&self.health),
421 }
422 }
423
424 pub(crate) fn delivery_guard(&self) -> OperationalSendGuard<'_> {
432 OperationalSendGuard {
433 stream: self,
434 armed: true,
435 }
436 }
437
438 pub(crate) async fn send(
439 &self,
440 connection_id: ConnectionId,
441 transport: Transport,
442 at: DateTime<Utc>,
443 kind: OperationalEventKind,
444 ) -> bool {
445 if self.health() == OperationalEventStreamHealth::Degraded {
446 return false;
447 }
448 let mut cancellation_guard = self.delivery_guard();
453 let permit = match self.sender.reserve().await {
456 Ok(permit) => permit,
457 Err(_) => {
458 self.mark_degraded(OperationalEventStreamFailure::SendFailed);
459 return false;
460 }
461 };
462 let Ok(sequence) =
463 self.next_sequence
464 .fetch_update(Ordering::AcqRel, Ordering::Acquire, |current| {
465 current.checked_add(1)
466 })
467 else {
468 self.mark_degraded(OperationalEventStreamFailure::SequenceExhausted);
469 return false;
470 };
471 permit.send(OperationalEvent {
472 sequence,
473 connection_id,
474 transport,
475 at,
476 kind,
477 });
478 cancellation_guard.disarm();
479 true
480 }
481
482 pub(crate) fn mark_degraded(&self, failure: OperationalEventStreamFailure) {
483 self.health.mark_degraded(failure);
484 }
485}
486
487#[cfg(test)]
488mod tests {
489 use super::*;
490 use crate::ids::MessageId;
491 use bytes::Bytes;
492 use rvoip_core_traits::data::DataReliability;
493
494 #[test]
495 fn debug_redacts_operational_payloads() {
496 let secret = "credential-like-secret";
497 let message = DataMessage {
498 label: secret.into(),
499 content_type: "application/secret".into(),
500 bytes: Bytes::from(secret),
501 reliability: DataReliability::ReliableOrdered,
502 message_id: MessageId::from_string(secret),
503 };
504 let values = [
505 OperationalEventKind::Dtmf {
506 digits: secret.into(),
507 duration_ms: 100,
508 },
509 OperationalEventKind::DataMessage { message },
510 OperationalEventKind::Transfer {
511 attempt_id: Some(TransferAttemptId::from_string(secret)),
512 target: OperationalTransferTarget::Uri,
513 outcome: OperationalTransferOutcome::Succeeded,
514 },
515 OperationalEventKind::TransferStatus {
516 attempt_id: Some(TransferAttemptId::from_string(secret)),
517 status: TransferStatus::Failed {
518 status_code: 503,
519 reason: secret.into(),
520 },
521 },
522 ];
523 for value in values {
524 let debug = format!("{value:?}");
525 assert!(!debug.contains(secret));
526 assert!(debug.contains("[redacted]"));
527 }
528 }
529
530 #[tokio::test]
531 async fn cancelled_backpressured_send_marks_stream_degraded() {
532 let (stream, _receiver) = OperationalEventStream::new(1);
533 let stream = std::sync::Arc::new(stream);
534 let mut health = stream.subscribe_health();
535 assert_eq!(
536 health.current(),
537 OperationalEventStreamHealth::Healthy,
538 "a new subscription exposes current health immediately"
539 );
540 assert!(
541 stream
542 .send(
543 ConnectionId::new(),
544 Transport::Sip,
545 Utc::now(),
546 OperationalEventKind::Connected,
547 )
548 .await
549 );
550 let blocked_stream = stream.clone();
551 let blocked = tokio::spawn(async move {
552 blocked_stream
553 .send(
554 ConnectionId::new(),
555 Transport::Sip,
556 Utc::now(),
557 OperationalEventKind::Connected,
558 )
559 .await
560 });
561 for _ in 0..10 {
562 tokio::task::yield_now().await;
563 }
564 assert_eq!(stream.sender.capacity(), 0);
565 assert!(!blocked.is_finished());
566 blocked.abort();
567 assert!(blocked.await.unwrap_err().is_cancelled());
568 let changed = tokio::time::timeout(std::time::Duration::from_secs(1), health.changed())
569 .await
570 .expect("cancellation publishes a health transition");
571 assert_eq!(changed, OperationalEventStreamHealth::Degraded);
572 assert_eq!(stream.health(), OperationalEventStreamHealth::Degraded);
573 assert_eq!(
574 stream.subscribe_health().current(),
575 OperationalEventStreamHealth::Degraded,
576 "degradation is retained for later subscribers"
577 );
578 }
579
580 #[tokio::test]
581 async fn cancelled_lifecycle_after_mutation_before_send_marks_stream_degraded() {
582 let (stream, _receiver) = OperationalEventStream::new(1);
583 let stream = std::sync::Arc::new(stream);
584 let (armed, armed_rx) = tokio::sync::oneshot::channel();
585 let (_release, release_rx) = tokio::sync::oneshot::channel::<()>();
586 let guarded_stream = std::sync::Arc::clone(&stream);
587 let lifecycle = tokio::spawn(async move {
588 let _delivery = guarded_stream.delivery_guard();
589 let _ = armed.send(());
590 let _ = release_rx.await;
594 });
595 armed_rx.await.unwrap();
596 lifecycle.abort();
597 assert!(lifecycle.await.unwrap_err().is_cancelled());
598 assert_eq!(stream.health(), OperationalEventStreamHealth::Degraded);
599 }
600
601 #[tokio::test]
602 async fn sequence_exhaustion_notifies_health_subscribers() {
603 let (stream, _receiver) = OperationalEventStream::new(1);
604 let mut health = stream.subscribe_health();
605 stream.next_sequence.store(u64::MAX, Ordering::Release);
606
607 assert!(
608 !stream
609 .send(
610 ConnectionId::new(),
611 Transport::Sip,
612 Utc::now(),
613 OperationalEventKind::Connected,
614 )
615 .await
616 );
617 let changed = tokio::time::timeout(std::time::Duration::from_secs(1), health.changed())
618 .await
619 .expect("sequence exhaustion publishes a health transition");
620 assert_eq!(changed, OperationalEventStreamHealth::Degraded);
621 }
622
623 #[tokio::test]
624 async fn failed_bounded_send_notifies_health_subscribers() {
625 let (stream, mut receiver) = OperationalEventStream::new(1);
626 let stream = Arc::new(stream);
627 let mut health = stream.subscribe_health();
628 assert!(
629 stream
630 .send(
631 ConnectionId::new(),
632 Transport::Sip,
633 Utc::now(),
634 OperationalEventKind::Connected,
635 )
636 .await
637 );
638
639 let blocked_stream = Arc::clone(&stream);
640 let blocked = tokio::spawn(async move {
641 blocked_stream
642 .send(
643 ConnectionId::new(),
644 Transport::Sip,
645 Utc::now(),
646 OperationalEventKind::Connected,
647 )
648 .await
649 });
650 tokio::time::timeout(std::time::Duration::from_secs(1), async {
651 while stream.sender.capacity() != 0 || blocked.is_finished() {
652 tokio::task::yield_now().await;
653 }
654 })
655 .await
656 .expect("second send waits for bounded capacity");
657
658 receiver.close();
659 assert!(!blocked.await.expect("sender task completed"));
660 let changed = tokio::time::timeout(std::time::Duration::from_secs(1), health.changed())
661 .await
662 .expect("send failure publishes a health transition");
663 assert_eq!(changed, OperationalEventStreamHealth::Degraded);
664 }
665}