use std::fmt;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::Arc;
use chrono::{DateTime, Utc};
use tokio::sync::{mpsc, watch};
use crate::adapter::{EndReason, TransferAttemptId, TransferStatus, TransferTarget};
use crate::connection::Transport;
use crate::ids::ConnectionId;
use crate::DataMessage;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum OperationalEventStreamHealth {
NotInstalled,
Healthy,
Degraded,
}
#[derive(Clone, Copy, Debug)]
pub(crate) enum OperationalEventStreamFailure {
ReceiverLost,
DeliveryCancelled,
SequenceExhausted,
SendFailed,
}
impl OperationalEventStreamFailure {
fn metric_label(self) -> &'static str {
match self {
Self::ReceiverLost => "receiver_lost",
Self::DeliveryCancelled => "delivery_cancelled",
Self::SequenceExhausted => "sequence_exhausted",
Self::SendFailed => "send_failed",
}
}
}
#[derive(Clone)]
pub struct OperationalEventStreamHealthSubscription {
updates: watch::Receiver<OperationalEventStreamHealth>,
receiver_closed: mpsc::Sender<OperationalEvent>,
health: Arc<OperationalEventStreamHealthState>,
}
impl OperationalEventStreamHealthSubscription {
pub fn current(&self) -> OperationalEventStreamHealth {
if self.receiver_closed.is_closed() {
self.health
.mark_degraded(OperationalEventStreamFailure::ReceiverLost);
}
self.health.current()
}
pub async fn changed(&mut self) -> OperationalEventStreamHealth {
let current = self.current();
if current == OperationalEventStreamHealth::Degraded {
self.updates.borrow_and_update();
return current;
}
tokio::select! {
changed = self.updates.changed() => {
if changed.is_err() {
self.health.mark_degraded(
OperationalEventStreamFailure::DeliveryCancelled,
);
}
}
() = self.receiver_closed.closed() => {
self.health.mark_degraded(
OperationalEventStreamFailure::ReceiverLost,
);
}
}
let current = self.health.current();
self.updates.borrow_and_update();
current
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum OperationalEndReason {
Normal,
Cancelled,
Failed,
Timeout,
BridgeTorn,
}
impl From<&EndReason> for OperationalEndReason {
fn from(reason: &EndReason) -> Self {
match reason {
EndReason::Normal => Self::Normal,
EndReason::Cancelled => Self::Cancelled,
EndReason::Failed { .. } => Self::Failed,
EndReason::Timeout => Self::Timeout,
EndReason::BridgeTorn => Self::BridgeTorn,
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum OperationalFailureReason {
AdapterReported,
CoreReported,
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum OperationalTransferTarget {
Uri,
Connection(ConnectionId),
Session(crate::ids::SessionId),
}
impl From<&TransferTarget> for OperationalTransferTarget {
fn from(target: &TransferTarget) -> Self {
match target {
TransferTarget::Uri(_) => Self::Uri,
TransferTarget::Connection(connection_id) => Self::Connection(connection_id.clone()),
TransferTarget::Session(session_id) => Self::Session(session_id.clone()),
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum OperationalTransferOutcome {
Submitted,
Succeeded,
Failed,
}
#[derive(Clone, Eq, PartialEq)]
#[non_exhaustive]
pub enum OperationalEventKind {
Connected,
Progress {
status_code: u16,
early_media: bool,
},
MediaActivity {
generation: u64,
},
Ended {
reason: OperationalEndReason,
},
Failed {
reason: OperationalFailureReason,
},
Dtmf {
digits: String,
duration_ms: u32,
},
DataMessage {
message: DataMessage,
},
Transfer {
attempt_id: Option<TransferAttemptId>,
target: OperationalTransferTarget,
outcome: OperationalTransferOutcome,
},
TransferStatus {
attempt_id: Option<TransferAttemptId>,
status: TransferStatus,
},
}
impl fmt::Debug for OperationalEventKind {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Connected => formatter.write_str("Connected"),
Self::Progress {
status_code,
early_media,
} => formatter
.debug_struct("Progress")
.field("status_code", status_code)
.field("early_media", early_media)
.finish(),
Self::MediaActivity { generation } => formatter
.debug_struct("MediaActivity")
.field("generation", generation)
.finish(),
Self::Ended { reason } => formatter
.debug_struct("Ended")
.field("reason", reason)
.finish(),
Self::Failed { reason } => formatter
.debug_struct("Failed")
.field("reason", reason)
.finish(),
Self::Dtmf { duration_ms, .. } => formatter
.debug_struct("Dtmf")
.field("digits", &"[redacted]")
.field("duration_ms", duration_ms)
.finish(),
Self::DataMessage { message } => formatter
.debug_struct("DataMessage")
.field("label", &"[redacted]")
.field("content_type", &"[redacted]")
.field("body_bytes", &message.bytes.len())
.field("message_id", &"[redacted]")
.field("reliability", &message.reliability)
.finish(),
Self::Transfer {
attempt_id,
target,
outcome,
} => formatter
.debug_struct("Transfer")
.field("attempt_id_present", &attempt_id.is_some())
.field("target", &RedactedTransferTarget(target))
.field("outcome", outcome)
.finish(),
Self::TransferStatus { attempt_id, status } => formatter
.debug_struct("TransferStatus")
.field("attempt_id_present", &attempt_id.is_some())
.field("status", status)
.finish(),
}
}
}
struct RedactedTransferTarget<'a>(&'a OperationalTransferTarget);
impl fmt::Debug for RedactedTransferTarget<'_> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.0 {
OperationalTransferTarget::Uri => formatter.write_str("Uri([redacted])"),
OperationalTransferTarget::Connection(_) => {
formatter.write_str("Connection([redacted])")
}
OperationalTransferTarget::Session(_) => formatter.write_str("Session([redacted])"),
}
}
}
#[derive(Clone, Eq, PartialEq)]
#[non_exhaustive]
pub struct OperationalEvent {
pub sequence: u64,
pub connection_id: ConnectionId,
pub transport: Transport,
pub at: DateTime<Utc>,
pub kind: OperationalEventKind,
}
impl fmt::Debug for OperationalEvent {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("OperationalEvent")
.field("sequence", &self.sequence)
.field("connection_id", &self.connection_id)
.field("transport", &self.transport)
.field("at", &self.at)
.field("kind", &self.kind)
.finish()
}
}
pub(crate) struct OperationalEventStream {
sender: mpsc::Sender<OperationalEvent>,
next_sequence: AtomicU64,
health: Arc<OperationalEventStreamHealthState>,
}
struct OperationalEventStreamHealthState {
degraded: AtomicBool,
updates: watch::Sender<OperationalEventStreamHealth>,
}
pub(crate) struct OperationalSendGuard<'a> {
stream: &'a OperationalEventStream,
armed: bool,
}
impl OperationalSendGuard<'_> {
pub(crate) fn disarm(&mut self) {
self.armed = false;
}
}
impl Drop for OperationalSendGuard<'_> {
fn drop(&mut self) {
if self.armed {
self.stream
.mark_degraded(OperationalEventStreamFailure::DeliveryCancelled);
}
}
}
impl OperationalEventStreamHealthState {
fn current(&self) -> OperationalEventStreamHealth {
if self.degraded.load(Ordering::Acquire) {
OperationalEventStreamHealth::Degraded
} else {
OperationalEventStreamHealth::Healthy
}
}
fn mark_degraded(&self, failure: OperationalEventStreamFailure) {
if !self.degraded.swap(true, Ordering::AcqRel) {
self.updates
.send_replace(OperationalEventStreamHealth::Degraded);
metrics::counter!(
"rvoip_core_operational_event_stream_failures_total",
"reason" => failure.metric_label()
)
.increment(1);
tracing::error!(
reason = failure.metric_label(),
"authoritative operational event stream degraded; failing closed"
);
}
}
}
impl OperationalEventStream {
pub(crate) fn new(capacity: usize) -> (Self, mpsc::Receiver<OperationalEvent>) {
let (sender, receiver) = mpsc::channel(capacity);
let (health_updates, _initial_health) =
watch::channel(OperationalEventStreamHealth::Healthy);
(
Self {
sender,
next_sequence: AtomicU64::new(1),
health: Arc::new(OperationalEventStreamHealthState {
degraded: AtomicBool::new(false),
updates: health_updates,
}),
},
receiver,
)
}
pub(crate) fn health(&self) -> OperationalEventStreamHealth {
if self.sender.is_closed() {
self.mark_degraded(OperationalEventStreamFailure::ReceiverLost);
}
self.health.current()
}
pub(crate) fn subscribe_health(&self) -> OperationalEventStreamHealthSubscription {
let _ = self.health();
OperationalEventStreamHealthSubscription {
updates: self.health.updates.subscribe(),
receiver_closed: self.sender.clone(),
health: Arc::clone(&self.health),
}
}
pub(crate) fn delivery_guard(&self) -> OperationalSendGuard<'_> {
OperationalSendGuard {
stream: self,
armed: true,
}
}
pub(crate) async fn send(
&self,
connection_id: ConnectionId,
transport: Transport,
at: DateTime<Utc>,
kind: OperationalEventKind,
) -> bool {
if self.health() == OperationalEventStreamHealth::Degraded {
return false;
}
let mut cancellation_guard = self.delivery_guard();
let permit = match self.sender.reserve().await {
Ok(permit) => permit,
Err(_) => {
self.mark_degraded(OperationalEventStreamFailure::SendFailed);
return false;
}
};
let Ok(sequence) =
self.next_sequence
.fetch_update(Ordering::AcqRel, Ordering::Acquire, |current| {
current.checked_add(1)
})
else {
self.mark_degraded(OperationalEventStreamFailure::SequenceExhausted);
return false;
};
permit.send(OperationalEvent {
sequence,
connection_id,
transport,
at,
kind,
});
cancellation_guard.disarm();
true
}
pub(crate) fn mark_degraded(&self, failure: OperationalEventStreamFailure) {
self.health.mark_degraded(failure);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ids::MessageId;
use bytes::Bytes;
use rvoip_core_traits::data::DataReliability;
#[test]
fn debug_redacts_operational_payloads() {
let secret = "credential-like-secret";
let message = DataMessage {
label: secret.into(),
content_type: "application/secret".into(),
bytes: Bytes::from(secret),
reliability: DataReliability::ReliableOrdered,
message_id: MessageId::from_string(secret),
};
let values = [
OperationalEventKind::Dtmf {
digits: secret.into(),
duration_ms: 100,
},
OperationalEventKind::DataMessage { message },
OperationalEventKind::Transfer {
attempt_id: Some(TransferAttemptId::from_string(secret)),
target: OperationalTransferTarget::Uri,
outcome: OperationalTransferOutcome::Succeeded,
},
OperationalEventKind::TransferStatus {
attempt_id: Some(TransferAttemptId::from_string(secret)),
status: TransferStatus::Failed {
status_code: 503,
reason: secret.into(),
},
},
];
for value in values {
let debug = format!("{value:?}");
assert!(!debug.contains(secret));
assert!(debug.contains("[redacted]"));
}
}
#[tokio::test]
async fn cancelled_backpressured_send_marks_stream_degraded() {
let (stream, _receiver) = OperationalEventStream::new(1);
let stream = std::sync::Arc::new(stream);
let mut health = stream.subscribe_health();
assert_eq!(
health.current(),
OperationalEventStreamHealth::Healthy,
"a new subscription exposes current health immediately"
);
assert!(
stream
.send(
ConnectionId::new(),
Transport::Sip,
Utc::now(),
OperationalEventKind::Connected,
)
.await
);
let blocked_stream = stream.clone();
let blocked = tokio::spawn(async move {
blocked_stream
.send(
ConnectionId::new(),
Transport::Sip,
Utc::now(),
OperationalEventKind::Connected,
)
.await
});
for _ in 0..10 {
tokio::task::yield_now().await;
}
assert_eq!(stream.sender.capacity(), 0);
assert!(!blocked.is_finished());
blocked.abort();
assert!(blocked.await.unwrap_err().is_cancelled());
let changed = tokio::time::timeout(std::time::Duration::from_secs(1), health.changed())
.await
.expect("cancellation publishes a health transition");
assert_eq!(changed, OperationalEventStreamHealth::Degraded);
assert_eq!(stream.health(), OperationalEventStreamHealth::Degraded);
assert_eq!(
stream.subscribe_health().current(),
OperationalEventStreamHealth::Degraded,
"degradation is retained for later subscribers"
);
}
#[tokio::test]
async fn cancelled_lifecycle_after_mutation_before_send_marks_stream_degraded() {
let (stream, _receiver) = OperationalEventStream::new(1);
let stream = std::sync::Arc::new(stream);
let (armed, armed_rx) = tokio::sync::oneshot::channel();
let (_release, release_rx) = tokio::sync::oneshot::channel::<()>();
let guarded_stream = std::sync::Arc::clone(&stream);
let lifecycle = tokio::spawn(async move {
let _delivery = guarded_stream.delivery_guard();
let _ = armed.send(());
let _ = release_rx.await;
});
armed_rx.await.unwrap();
lifecycle.abort();
assert!(lifecycle.await.unwrap_err().is_cancelled());
assert_eq!(stream.health(), OperationalEventStreamHealth::Degraded);
}
#[tokio::test]
async fn sequence_exhaustion_notifies_health_subscribers() {
let (stream, _receiver) = OperationalEventStream::new(1);
let mut health = stream.subscribe_health();
stream.next_sequence.store(u64::MAX, Ordering::Release);
assert!(
!stream
.send(
ConnectionId::new(),
Transport::Sip,
Utc::now(),
OperationalEventKind::Connected,
)
.await
);
let changed = tokio::time::timeout(std::time::Duration::from_secs(1), health.changed())
.await
.expect("sequence exhaustion publishes a health transition");
assert_eq!(changed, OperationalEventStreamHealth::Degraded);
}
#[tokio::test]
async fn failed_bounded_send_notifies_health_subscribers() {
let (stream, mut receiver) = OperationalEventStream::new(1);
let stream = Arc::new(stream);
let mut health = stream.subscribe_health();
assert!(
stream
.send(
ConnectionId::new(),
Transport::Sip,
Utc::now(),
OperationalEventKind::Connected,
)
.await
);
let blocked_stream = Arc::clone(&stream);
let blocked = tokio::spawn(async move {
blocked_stream
.send(
ConnectionId::new(),
Transport::Sip,
Utc::now(),
OperationalEventKind::Connected,
)
.await
});
tokio::time::timeout(std::time::Duration::from_secs(1), async {
while stream.sender.capacity() != 0 || blocked.is_finished() {
tokio::task::yield_now().await;
}
})
.await
.expect("second send waits for bounded capacity");
receiver.close();
assert!(!blocked.await.expect("sender task completed"));
let changed = tokio::time::timeout(std::time::Duration::from_secs(1), health.changed())
.await
.expect("send failure publishes a health transition");
assert_eq!(changed, OperationalEventStreamHealth::Degraded);
}
}