use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::{mpsc, watch};
use crate::event::AmiEvent;
use asterisk_rs_core::event::EventSubscription;
#[derive(Debug, Clone)]
pub struct CompletedCall {
pub channel: String,
pub unique_id: String,
pub linked_id: String,
pub start_time: Instant,
pub end_time: Instant,
pub duration: Duration,
pub cause: u32,
pub cause_txt: String,
pub events: Vec<AmiEvent>,
}
struct ActiveCall {
channel: String,
unique_id: String,
linked_id: String,
start_time: Instant,
events: Vec<AmiEvent>,
}
pub struct CallTracker {
shutdown_tx: watch::Sender<bool>,
task_handle: tokio::task::JoinHandle<()>,
dropped_count: Arc<AtomicU64>,
}
impl std::fmt::Debug for CallTracker {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CallTracker").finish_non_exhaustive()
}
}
impl CallTracker {
pub fn new(subscription: EventSubscription<AmiEvent>) -> (Self, mpsc::Receiver<CompletedCall>) {
let (completed_tx, completed_rx) = mpsc::channel(256);
let (shutdown_tx, shutdown_rx) = watch::channel(false);
let dropped_count = Arc::new(AtomicU64::new(0));
let task_handle = tokio::spawn(track_loop(
subscription,
completed_tx,
shutdown_rx,
DEFAULT_CALL_TTL,
Arc::clone(&dropped_count),
));
let tracker = Self {
shutdown_tx,
task_handle,
dropped_count,
};
(tracker, completed_rx)
}
pub fn dropped_count(&self) -> u64 {
self.dropped_count.load(Ordering::Relaxed)
}
pub fn shutdown(&self) {
let _ = self.shutdown_tx.send(true);
self.task_handle.abort();
}
}
impl Drop for CallTracker {
fn drop(&mut self) {
self.shutdown();
}
}
const DEFAULT_CALL_TTL: Duration = Duration::from_secs(3600);
async fn track_loop(
mut subscription: EventSubscription<AmiEvent>,
completed_tx: mpsc::Sender<CompletedCall>,
mut shutdown_rx: watch::Receiver<bool>,
ttl: Duration,
dropped_count: Arc<AtomicU64>,
) {
let mut active: HashMap<String, ActiveCall> = HashMap::new();
loop {
tokio::select! {
event = subscription.recv() => {
let Some(event) = event else { break };
evict_stale(&mut active, &completed_tx, ttl, &dropped_count);
handle_event(&mut active, &completed_tx, event, &dropped_count);
}
_ = shutdown_rx.changed() => {
break;
}
}
}
}
fn handle_event(
active: &mut HashMap<String, ActiveCall>,
completed_tx: &mpsc::Sender<CompletedCall>,
event: AmiEvent,
dropped_count: &AtomicU64,
) {
if let AmiEvent::NewChannel {
ref channel,
ref unique_id,
ref linked_id,
..
} = event
{
let call = ActiveCall {
channel: channel.clone(),
unique_id: unique_id.clone(),
linked_id: linked_id.clone(),
start_time: Instant::now(),
events: vec![event],
};
active.insert(call.unique_id.clone(), call);
return;
}
if let AmiEvent::Rename {
ref unique_id,
ref new_name,
..
} = event
{
if let Some(call) = active.get_mut(unique_id.as_str()) {
call.channel = new_name.clone();
call.events.push(event);
}
return;
}
if let AmiEvent::Hangup {
ref unique_id,
cause,
ref cause_txt,
..
} = event
{
if let Some(mut call) = active.remove(unique_id.as_str()) {
let end_time = Instant::now();
let cause_txt = cause_txt.clone();
call.events.push(event);
let completed = CompletedCall {
channel: call.channel,
unique_id: call.unique_id,
linked_id: call.linked_id,
start_time: call.start_time,
end_time,
duration: end_time.duration_since(call.start_time),
cause,
cause_txt,
events: call.events,
};
if completed_tx.try_send(completed).is_err() {
dropped_count.fetch_add(1, Ordering::Relaxed);
tracing::warn!("completed_tx full or closed, dropping completed call");
}
}
return;
}
if let Some(uid) = extract_unique_id(&event) {
if let Some(call) = active.get_mut(uid) {
call.events.push(event);
}
}
}
fn extract_unique_id(event: &AmiEvent) -> Option<&str> {
match event {
AmiEvent::NewChannel { unique_id, .. }
| AmiEvent::Hangup { unique_id, .. }
| AmiEvent::Newstate { unique_id, .. }
| AmiEvent::DialBegin { unique_id, .. }
| AmiEvent::DialEnd { unique_id, .. }
| AmiEvent::DtmfBegin { unique_id, .. }
| AmiEvent::DtmfEnd { unique_id, .. }
| AmiEvent::BridgeEnter { unique_id, .. }
| AmiEvent::BridgeLeave { unique_id, .. }
| AmiEvent::VarSet { unique_id, .. }
| AmiEvent::Hold { unique_id, .. }
| AmiEvent::Unhold { unique_id, .. }
| AmiEvent::HangupRequest { unique_id, .. }
| AmiEvent::SoftHangupRequest { unique_id, .. }
| AmiEvent::NewExten { unique_id, .. }
| AmiEvent::NewCallerid { unique_id, .. }
| AmiEvent::NewConnectedLine { unique_id, .. }
| AmiEvent::NewAccountCode { unique_id, .. }
| AmiEvent::Rename { unique_id, .. }
| AmiEvent::OriginateResponse { unique_id, .. }
| AmiEvent::DialState { unique_id, .. }
| AmiEvent::Flash { unique_id, .. }
| AmiEvent::Wink { unique_id, .. }
| AmiEvent::BridgeInfoChannel { unique_id, .. }
| AmiEvent::LocalBridge { unique_id, .. }
| AmiEvent::LocalOptimizationBegin { unique_id, .. }
| AmiEvent::LocalOptimizationEnd { unique_id, .. }
| AmiEvent::Cdr { unique_id, .. }
| AmiEvent::Cel { unique_id, .. }
| AmiEvent::QueueCallerAbandon { unique_id, .. }
| AmiEvent::QueueCallerJoin { unique_id, .. }
| AmiEvent::QueueCallerLeave { unique_id, .. }
| AmiEvent::QueueEntry { unique_id, .. }
| AmiEvent::AgentCalled { unique_id, .. }
| AmiEvent::AgentConnect { unique_id, .. }
| AmiEvent::AgentComplete { unique_id, .. }
| AmiEvent::AgentDump { unique_id, .. }
| AmiEvent::AgentLogin { unique_id, .. }
| AmiEvent::AgentRingNoAnswer { unique_id, .. }
| AmiEvent::ConfbridgeJoin { unique_id, .. }
| AmiEvent::ConfbridgeLeave { unique_id, .. }
| AmiEvent::ConfbridgeList { unique_id, .. }
| AmiEvent::ConfbridgeMute { unique_id, .. }
| AmiEvent::ConfbridgeUnmute { unique_id, .. }
| AmiEvent::ConfbridgeTalking { unique_id, .. }
| AmiEvent::MixMonitorStart { unique_id, .. }
| AmiEvent::MixMonitorStop { unique_id, .. }
| AmiEvent::MixMonitorMute { unique_id, .. }
| AmiEvent::MusicOnHoldStart { unique_id, .. }
| AmiEvent::MusicOnHoldStop { unique_id, .. }
| AmiEvent::ParkedCall { unique_id, .. }
| AmiEvent::ParkedCallGiveUp { unique_id, .. }
| AmiEvent::ParkedCallTimeOut { unique_id, .. }
| AmiEvent::ParkedCallSwap { unique_id, .. }
| AmiEvent::UnParkedCall { unique_id, .. }
| AmiEvent::Pickup { unique_id, .. }
| AmiEvent::ChanSpyStart { unique_id, .. }
| AmiEvent::ChanSpyStop { unique_id, .. }
| AmiEvent::ChannelTalkingStart { unique_id, .. }
| AmiEvent::ChannelTalkingStop { unique_id, .. }
| AmiEvent::RTCPReceived { unique_id, .. }
| AmiEvent::RTCPSent { unique_id, .. }
| AmiEvent::AsyncAGIStart { unique_id, .. }
| AmiEvent::AsyncAGIExec { unique_id, .. }
| AmiEvent::AsyncAGIEnd { unique_id, .. }
| AmiEvent::AGIExecStart { unique_id, .. }
| AmiEvent::AGIExecEnd { unique_id, .. }
| AmiEvent::HangupHandlerPush { unique_id, .. }
| AmiEvent::HangupHandlerPop { unique_id, .. }
| AmiEvent::HangupHandlerRun { unique_id, .. }
| AmiEvent::Status { unique_id, .. }
| AmiEvent::CoreShowChannel { unique_id, .. }
| AmiEvent::AocD { unique_id, .. }
| AmiEvent::AocE { unique_id, .. }
| AmiEvent::AocS { unique_id, .. }
| AmiEvent::FAXStatus { unique_id, .. }
| AmiEvent::ReceiveFAX { unique_id, .. }
| AmiEvent::SendFAX { unique_id, .. }
| AmiEvent::MeetmeJoin { unique_id, .. }
| AmiEvent::MeetmeLeave { unique_id, .. }
| AmiEvent::MeetmeMute { unique_id, .. }
| AmiEvent::MeetmeTalking { unique_id, .. }
| AmiEvent::MeetmeTalkRequest { unique_id, .. }
| AmiEvent::MeetmeList { unique_id, .. }
| AmiEvent::MiniVoiceMail { unique_id, .. }
| AmiEvent::FAXSession { unique_id, .. }
| AmiEvent::MCID { unique_id, .. } => Some(unique_id.as_str()),
AmiEvent::AttendedTransfer {
transferer_unique_id,
..
} => Some(transferer_unique_id.as_str()),
AmiEvent::BlindTransfer {
transferer_unique_id,
..
} => Some(transferer_unique_id.as_str()),
AmiEvent::UserEvent { unique_id, .. } => unique_id.as_deref(),
AmiEvent::DAHDIChannel { unique_id, .. } => unique_id.as_deref(),
AmiEvent::FullyBooted { .. }
| AmiEvent::PeerStatus { .. }
| AmiEvent::BridgeCreate { .. }
| AmiEvent::BridgeDestroy { .. }
| AmiEvent::BridgeMerge { .. }
| AmiEvent::BridgeInfoComplete { .. }
| AmiEvent::BridgeVideoSourceUpdate { .. }
| AmiEvent::QueueMemberAdded { .. }
| AmiEvent::QueueMemberRemoved { .. }
| AmiEvent::QueueMemberPause { .. }
| AmiEvent::QueueMemberStatus { .. }
| AmiEvent::QueueMemberPenalty { .. }
| AmiEvent::QueueMemberRinginuse { .. }
| AmiEvent::QueueParams { .. }
| AmiEvent::AgentLogoff { .. }
| AmiEvent::Agents { .. }
| AmiEvent::AgentsComplete
| AmiEvent::ConfbridgeStart { .. }
| AmiEvent::ConfbridgeEnd { .. }
| AmiEvent::ConfbridgeRecord { .. }
| AmiEvent::ConfbridgeStopRecord { .. }
| AmiEvent::ConfbridgeListRooms { .. }
| AmiEvent::DeviceStateChange { .. }
| AmiEvent::ExtensionStatus { .. }
| AmiEvent::PresenceStateChange { .. }
| AmiEvent::PresenceStatus { .. }
| AmiEvent::ContactStatus { .. }
| AmiEvent::Registry { .. }
| AmiEvent::MessageWaiting { .. }
| AmiEvent::VoicemailPasswordChange { .. }
| AmiEvent::FailedACL { .. }
| AmiEvent::InvalidAccountID { .. }
| AmiEvent::InvalidPassword { .. }
| AmiEvent::ChallengeResponseFailed { .. }
| AmiEvent::ChallengeSent { .. }
| AmiEvent::SuccessfulAuth { .. }
| AmiEvent::SessionLimit { .. }
| AmiEvent::UnexpectedAddress { .. }
| AmiEvent::RequestBadFormat { .. }
| AmiEvent::RequestNotAllowed { .. }
| AmiEvent::RequestNotSupported { .. }
| AmiEvent::InvalidTransport { .. }
| AmiEvent::AuthMethodNotAllowed { .. }
| AmiEvent::Shutdown { .. }
| AmiEvent::Reload { .. }
| AmiEvent::Load { .. }
| AmiEvent::Unload { .. }
| AmiEvent::LogChannel { .. }
| AmiEvent::LoadAverageLimit
| AmiEvent::MemoryLimit
| AmiEvent::StatusComplete { .. }
| AmiEvent::CoreShowChannelsComplete { .. }
| AmiEvent::CoreShowChannelMapComplete
| AmiEvent::Alarm { .. }
| AmiEvent::AlarmClear { .. }
| AmiEvent::SpanAlarm { .. }
| AmiEvent::SpanAlarmClear { .. }
| AmiEvent::MeetmeEnd { .. }
| AmiEvent::MeetmeListRooms { .. }
| AmiEvent::DeviceStateListComplete { .. }
| AmiEvent::ExtensionStateListComplete { .. }
| AmiEvent::PresenceStateListComplete { .. }
| AmiEvent::AorDetail { .. }
| AmiEvent::AorList { .. }
| AmiEvent::AorListComplete { .. }
| AmiEvent::AuthDetail { .. }
| AmiEvent::AuthList { .. }
| AmiEvent::AuthListComplete { .. }
| AmiEvent::ContactList { .. }
| AmiEvent::ContactListComplete { .. }
| AmiEvent::ContactStatusDetail { .. }
| AmiEvent::EndpointDetail { .. }
| AmiEvent::EndpointDetailComplete { .. }
| AmiEvent::EndpointList { .. }
| AmiEvent::EndpointListComplete { .. }
| AmiEvent::IdentifyDetail { .. }
| AmiEvent::TransportDetail { .. }
| AmiEvent::ResourceListDetail { .. }
| AmiEvent::InboundRegistrationDetail { .. }
| AmiEvent::OutboundRegistrationDetail { .. }
| AmiEvent::InboundSubscriptionDetail { .. }
| AmiEvent::OutboundSubscriptionDetail { .. }
| AmiEvent::MWIGet { .. }
| AmiEvent::MWIGetComplete { .. }
| AmiEvent::FAXSessionsEntry { .. }
| AmiEvent::FAXSessionsComplete { .. }
| AmiEvent::FAXStats { .. }
| AmiEvent::DNDState { .. }
| AmiEvent::DeadlockStart
| AmiEvent::Unknown { .. } => None,
}
}
fn evict_stale(
active: &mut HashMap<String, ActiveCall>,
completed_tx: &mpsc::Sender<CompletedCall>,
ttl: Duration,
dropped_count: &AtomicU64,
) {
let now = Instant::now();
active.retain(|_, call| {
if now.duration_since(call.start_time) <= ttl {
return true;
}
let completed = CompletedCall {
channel: call.channel.clone(),
unique_id: call.unique_id.clone(),
linked_id: call.linked_id.clone(),
start_time: call.start_time,
end_time: now,
duration: now.duration_since(call.start_time),
cause: 0,
cause_txt: "ttl eviction: no hangup received".to_string(),
events: std::mem::take(&mut call.events),
};
if completed_tx.try_send(completed).is_err() {
dropped_count.fetch_add(1, Ordering::Relaxed);
tracing::warn!(unique_id = %call.unique_id, "completed_tx full, dropping stale evicted call");
}
false
});
}