use std::sync::Arc;
use std::time::Duration;
use tokio::sync::RwLock;
use tracing::debug;
use super::types::QueueStats;
#[derive(Debug)]
pub struct StatsManager {
cache: Arc<RwLock<QueueStats>>,
cache_dirty: Arc<RwLock<bool>>,
}
impl StatsManager {
pub fn new() -> Self {
Self {
cache: Arc::new(RwLock::new(QueueStats::new())),
cache_dirty: Arc::new(RwLock::new(false)),
}
}
pub async fn get_stats(&self) -> QueueStats {
self.cache.read().await.clone()
}
pub async fn update_stats(&self, stats: QueueStats) {
let mut cache = self.cache.write().await;
*cache = stats;
let mut dirty = self.cache_dirty.write().await;
*dirty = false;
debug!("Updated statistics cache");
}
pub async fn mark_dirty(&self) {
let mut dirty = self.cache_dirty.write().await;
*dirty = true;
}
pub async fn is_dirty(&self) -> bool {
*self.cache_dirty.read().await
}
pub async fn reset(&self) {
let mut cache = self.cache.write().await;
*cache = QueueStats::new();
let mut dirty = self.cache_dirty.write().await;
*dirty = false;
debug!("Reset statistics cache");
}
}
impl Default for StatsManager {
fn default() -> Self {
Self::new()
}
}
pub struct StatsCalculator;
impl StatsCalculator {
#[allow(clippy::too_many_arguments)]
pub fn calculate_stats(
pending_count: usize,
in_progress_count: usize,
completed_count: usize,
failed_count: usize,
abandoned_count: usize,
total_added: u64,
duplicate_count: u64,
created_at: chrono::DateTime<chrono::Utc>,
) -> QueueStats {
QueueStats {
total_added,
pending_count: pending_count as u64,
in_progress_count: in_progress_count as u64,
completed_count: completed_count as u64,
failed_count: failed_count as u64,
abandoned_count: abandoned_count as u64,
duplicate_count,
avg_processing_time: Duration::ZERO, created_at,
}
}
pub fn calculate_success_rate(completed: u64, abandoned: u64) -> f64 {
let total_processed = completed + abandoned;
if total_processed == 0 {
0.0
} else {
(completed as f64 / total_processed as f64) * 100.0
}
}
pub fn calculate_worker_utilization(in_progress: u64, max_workers: u32) -> f64 {
if max_workers == 0 {
0.0
} else {
(in_progress as f64 / max_workers as f64).min(1.0) * 100.0
}
}
pub fn calculate_health_score(stats: &QueueStats, max_workers: u32) -> f64 {
let success_rate = stats.success_rate();
let utilization = stats.worker_utilization(max_workers);
let active_ratio = if stats.total_added > 0 {
(stats.active_count() as f64 / stats.total_added as f64) * 100.0
} else {
0.0
};
let health = (success_rate * 0.5) + (utilization * 0.3) + ((100.0 - active_ratio) * 0.2);
health.clamp(0.0, 100.0)
}
}
#[derive(Debug, Clone)]
pub struct PerformanceMetrics {
pub items_per_second: f64,
pub avg_processing_time: Duration,
pub throughput: f64,
pub memory_usage_mb: f64,
}
impl PerformanceMetrics {
pub fn new() -> Self {
Self {
items_per_second: 0.0,
avg_processing_time: Duration::ZERO,
throughput: 0.0,
memory_usage_mb: 0.0,
}
}
pub fn calculate_from_stats(stats: &QueueStats, duration: Duration) -> Self {
let total_processed = stats.completed_count + stats.abandoned_count;
let items_per_second = if duration.as_secs() > 0 {
total_processed as f64 / duration.as_secs() as f64
} else {
0.0
};
let throughput = if stats.in_progress_count > 0 {
items_per_second
} else {
0.0
};
let active_items = stats.active_count();
let memory_usage_mb = (active_items as f64 * 1024.0) / (1024.0 * 1024.0);
Self {
items_per_second,
avg_processing_time: stats.avg_processing_time,
throughput,
memory_usage_mb,
}
}
}
impl Default for PerformanceMetrics {
fn default() -> Self {
Self::new()
}
}
pub struct StatsReporter;
impl StatsReporter {
pub fn generate_summary_report(stats: &QueueStats, max_workers: u32) -> String {
let health_score = StatsCalculator::calculate_health_score(stats, max_workers);
format!(
"Queue Statistics Summary:\n\
├─ Total Added: {}\n\
├─ Completed: {} ({:.1}%)\n\
├─ Failed: {}\n\
├─ Abandoned: {}\n\
├─ In Progress: {}\n\
├─ Pending: {}\n\
├─ Duplicates: {}\n\
├─ Success Rate: {:.1}%\n\
├─ Worker Utilization: {:.1}%\n\
└─ Health Score: {:.1}/100",
stats.total_added,
stats.completed_count,
if stats.total_added > 0 {
(stats.completed_count as f64 / stats.total_added as f64) * 100.0
} else {
0.0
},
stats.failed_count,
stats.abandoned_count,
stats.in_progress_count,
stats.pending_count,
stats.duplicate_count,
stats.success_rate(),
stats.worker_utilization(max_workers),
health_score
)
}
pub fn generate_detailed_report(
stats: &QueueStats,
max_workers: u32,
performance: &PerformanceMetrics,
) -> String {
let summary = Self::generate_summary_report(stats, max_workers);
format!(
"{}\n\n\
Performance Metrics:\n\
├─ Items/Second: {:.2}\n\
├─ Avg Processing Time: {:?}\n\
├─ Throughput: {:.2}\n\
├─ Memory Usage: {:.2} MB\n\
└─ Queue Age: {:?}",
summary,
performance.items_per_second,
performance.avg_processing_time,
performance.throughput,
performance.memory_usage_mb,
chrono::Utc::now().signed_duration_since(stats.created_at)
)
}
pub fn generate_compact_status(stats: &QueueStats) -> String {
format!(
"Queue: {} total, {} completed, {} pending, {} in progress, {} failed",
stats.total_added,
stats.completed_count,
stats.pending_count,
stats.in_progress_count,
stats.failed_count
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Utc;
use std::time::Duration;
#[test]
fn test_stats_manager_creation() {
let manager = StatsManager::new();
assert!(format!("{:?}", manager).contains("StatsManager"));
}
#[tokio::test]
async fn test_stats_manager_operations() {
let manager = StatsManager::new();
let initial_stats = manager.get_stats().await;
assert_eq!(initial_stats.total_added, 0);
assert!(!manager.is_dirty().await);
let mut new_stats = QueueStats::new();
new_stats.total_added = 100;
new_stats.completed_count = 80;
manager.update_stats(new_stats.clone()).await;
let updated_stats = manager.get_stats().await;
assert_eq!(updated_stats.total_added, 100);
assert_eq!(updated_stats.completed_count, 80);
manager.mark_dirty().await;
assert!(manager.is_dirty().await);
manager.reset().await;
let reset_stats = manager.get_stats().await;
assert_eq!(reset_stats.total_added, 0);
assert!(!manager.is_dirty().await);
}
#[test]
fn test_stats_calculator() {
let stats = StatsCalculator::calculate_stats(
10, 5, 80, 3, 2, 100, 5, Utc::now(),
);
assert_eq!(stats.pending_count, 10);
assert_eq!(stats.in_progress_count, 5);
assert_eq!(stats.completed_count, 80);
assert_eq!(stats.failed_count, 3);
assert_eq!(stats.abandoned_count, 2);
assert_eq!(stats.total_added, 100);
assert_eq!(stats.duplicate_count, 5);
}
#[test]
fn test_success_rate_calculation() {
assert_eq!(StatsCalculator::calculate_success_rate(80, 20), 80.0);
assert_eq!(StatsCalculator::calculate_success_rate(0, 0), 0.0);
assert_eq!(StatsCalculator::calculate_success_rate(100, 0), 100.0);
}
#[test]
fn test_worker_utilization_calculation() {
assert_eq!(StatsCalculator::calculate_worker_utilization(5, 10), 50.0);
assert_eq!(StatsCalculator::calculate_worker_utilization(10, 10), 100.0);
assert_eq!(StatsCalculator::calculate_worker_utilization(15, 10), 100.0);
assert_eq!(StatsCalculator::calculate_worker_utilization(5, 0), 0.0);
}
#[test]
fn test_health_score_calculation() {
let mut stats = QueueStats::new();
stats.total_added = 100;
stats.completed_count = 80;
stats.abandoned_count = 10;
stats.in_progress_count = 5;
stats.pending_count = 5;
let health = StatsCalculator::calculate_health_score(&stats, 10);
assert!((0.0..=100.0).contains(&health));
}
#[test]
fn test_performance_metrics() {
let stats = QueueStats {
total_added: 100,
completed_count: 80,
abandoned_count: 10,
in_progress_count: 5,
pending_count: 5,
..QueueStats::new()
};
let performance = PerformanceMetrics::calculate_from_stats(&stats, Duration::from_secs(60));
assert!(performance.items_per_second > 0.0);
assert!(performance.memory_usage_mb >= 0.0);
}
#[test]
fn test_stats_reporter() {
let stats = QueueStats {
total_added: 100,
completed_count: 80,
abandoned_count: 10,
in_progress_count: 5,
pending_count: 5,
failed_count: 0,
duplicate_count: 2,
..QueueStats::new()
};
let summary = StatsReporter::generate_summary_report(&stats, 10);
assert!(summary.contains("Total Added: 100"));
assert!(summary.contains("Completed: 80"));
assert!(summary.contains("Success Rate:"));
let compact = StatsReporter::generate_compact_status(&stats);
assert!(compact.contains("100 total"));
assert!(compact.contains("80 completed"));
}
}