use std::io;
use std::io::Write;
use async_channel::Receiver;
use indicatif::{HumanBytes, HumanCount, HumanDuration, ProgressBar, ProgressStyle};
use s3rm_rs::types::DeletionStatistics;
use simple_moving_average::{SMA, SumTreeSMA};
use tokio::task::JoinHandle;
use tokio::time::Instant;
use tracing::info;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IndicatorSummary {
pub total_delete_count: u64,
pub total_delete_bytes: u64,
pub total_error_count: u64,
pub total_skip_count: u64,
}
const MOVING_AVERAGE_PERIOD_SECS: usize = 10;
const REFRESH_INTERVAL: f32 = 1.0;
pub fn show_indicator(
stats_receiver: Receiver<DeletionStatistics>,
show_progress: bool,
show_result: bool,
dry_run: bool,
) -> JoinHandle<IndicatorSummary> {
let progress_style =
ProgressStyle::with_template("{wide_msg}").expect("hardcoded progress template invalid");
let progress_text = ProgressBar::new(0);
progress_text.set_style(progress_style);
tokio::spawn(async move {
let start_time = Instant::now();
let mut ma_deleted_count = SumTreeSMA::<_, u64, MOVING_AVERAGE_PERIOD_SECS>::new();
let mut total_delete_count: u64 = 0;
let mut total_delete_bytes: u64 = 0;
let mut total_error_count: u64 = 0;
let mut total_skip_count: u64 = 0;
loop {
let mut period_count: u64 = 0;
let period = Instant::now();
loop {
while let Ok(stats) = stats_receiver.try_recv() {
match stats {
DeletionStatistics::DeleteComplete { .. } => {
period_count += 1;
total_delete_count += 1;
}
DeletionStatistics::DeleteBytes(size) => {
total_delete_bytes += size;
}
DeletionStatistics::DeleteError { .. } => {
total_error_count += 1;
}
DeletionStatistics::DeleteSkip { .. } => {
total_skip_count += 1;
}
}
}
if REFRESH_INTERVAL < period.elapsed().as_secs_f32() {
break;
}
if stats_receiver.is_closed() {
let elapsed = start_time.elapsed();
let elapsed_secs_f64 = elapsed.as_secs_f64();
let mut objects_per_sec = (total_delete_count as f64 / elapsed_secs_f64) as u64;
if elapsed_secs_f64 < REFRESH_INTERVAL as f64 {
objects_per_sec = total_delete_count;
}
if dry_run {
objects_per_sec = 0;
}
info!(
message = "deletion summary",
deleted_bytes = total_delete_bytes,
deleted_objects = total_delete_count,
deleted_objects_per_sec = objects_per_sec,
skipped = total_skip_count,
error = total_error_count,
duration_sec = elapsed_secs_f64,
);
if show_result {
progress_text.set_style(
ProgressStyle::with_template("{msg}")
.expect("hardcoded progress template invalid"),
);
progress_text.finish_with_message(format!(
"deleted {:>3} objects | {:>3} objects/sec, skipped {} objects, error {} objects, deleted {:>3}, duration {}",
total_delete_count,
HumanCount(objects_per_sec),
total_skip_count,
total_error_count,
HumanBytes(total_delete_bytes),
HumanDuration(elapsed),
));
eprintln!();
let _ = io::stderr().flush();
}
return IndicatorSummary {
total_delete_count,
total_delete_bytes,
total_error_count,
total_skip_count,
};
}
tokio::time::sleep(std::time::Duration::from_secs_f32(0.05)).await;
}
if !dry_run {
ma_deleted_count.add_sample(period_count);
}
if show_progress {
progress_text.set_message(format!(
"deleted {:>3} objects | {:>3} objects/sec, skipped {} objects, error {} objects, deleted {:>3}",
total_delete_count,
HumanCount(ma_deleted_count.get_average()),
total_skip_count,
total_error_count,
HumanBytes(total_delete_bytes),
));
}
}
})
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn show_indicator_empty_channel_completes() {
let (sender, receiver) = async_channel::unbounded();
drop(sender);
let handle = show_indicator(receiver, false, false, false);
let summary = tokio::time::timeout(std::time::Duration::from_secs(5), handle)
.await
.expect("indicator should complete within timeout")
.expect("indicator task should not panic");
assert_eq!(summary.total_delete_count, 0);
assert_eq!(summary.total_delete_bytes, 0);
assert_eq!(summary.total_error_count, 0);
assert_eq!(summary.total_skip_count, 0);
}
#[tokio::test]
async fn show_indicator_with_stats_completes() {
let (sender, receiver) = async_channel::unbounded();
sender
.send(DeletionStatistics::DeleteComplete {
key: "test/obj1".to_string(),
})
.await
.unwrap();
sender
.send(DeletionStatistics::DeleteBytes(1024))
.await
.unwrap();
sender
.send(DeletionStatistics::DeleteError {
key: "test/obj2".to_string(),
})
.await
.unwrap();
sender
.send(DeletionStatistics::DeleteSkip {
key: "test/obj3".to_string(),
})
.await
.unwrap();
drop(sender);
let handle = show_indicator(receiver, false, false, false);
let summary = tokio::time::timeout(std::time::Duration::from_secs(5), handle)
.await
.expect("indicator should complete within timeout")
.expect("indicator task should not panic");
assert_eq!(summary.total_delete_count, 1);
assert_eq!(summary.total_delete_bytes, 1024);
assert_eq!(summary.total_error_count, 1);
assert_eq!(summary.total_skip_count, 1);
}
#[tokio::test]
async fn show_indicator_dry_run_mode() {
let (sender, receiver) = async_channel::unbounded();
sender
.send(DeletionStatistics::DeleteComplete {
key: "test/obj1".to_string(),
})
.await
.unwrap();
sender
.send(DeletionStatistics::DeleteBytes(2048))
.await
.unwrap();
drop(sender);
let handle = show_indicator(receiver, false, false, true);
let summary = tokio::time::timeout(std::time::Duration::from_secs(5), handle)
.await
.expect("indicator should complete within timeout")
.expect("indicator task should not panic");
assert_eq!(summary.total_delete_count, 1);
assert_eq!(summary.total_delete_bytes, 2048);
}
#[tokio::test]
async fn show_indicator_show_result_prints_summary() {
let (sender, receiver) = async_channel::unbounded();
sender
.send(DeletionStatistics::DeleteComplete {
key: "obj1".to_string(),
})
.await
.unwrap();
sender
.send(DeletionStatistics::DeleteBytes(512))
.await
.unwrap();
drop(sender);
let handle = show_indicator(receiver, false, true, false);
tokio::time::timeout(std::time::Duration::from_secs(5), handle)
.await
.expect("indicator should complete within timeout")
.expect("indicator task should not panic");
}
#[tokio::test]
async fn show_indicator_moving_average_updated_after_refresh_interval() {
let (sender, receiver) = async_channel::unbounded();
sender
.send(DeletionStatistics::DeleteComplete {
key: "obj1".to_string(),
})
.await
.unwrap();
sender
.send(DeletionStatistics::DeleteBytes(100))
.await
.unwrap();
let handle = show_indicator(receiver, false, false, false);
tokio::time::sleep(std::time::Duration::from_millis(1200)).await;
drop(sender);
let summary = tokio::time::timeout(std::time::Duration::from_secs(5), handle)
.await
.expect("indicator should complete within timeout")
.expect("indicator task should not panic");
assert_eq!(summary.total_delete_count, 1);
assert_eq!(summary.total_delete_bytes, 100);
}
#[tokio::test]
async fn show_indicator_progress_text_updated_after_refresh_interval() {
let (sender, receiver) = async_channel::unbounded();
sender
.send(DeletionStatistics::DeleteComplete {
key: "obj1".to_string(),
})
.await
.unwrap();
sender
.send(DeletionStatistics::DeleteBytes(256))
.await
.unwrap();
sender
.send(DeletionStatistics::DeleteError {
key: "obj2".to_string(),
})
.await
.unwrap();
sender
.send(DeletionStatistics::DeleteSkip {
key: "obj3".to_string(),
})
.await
.unwrap();
let handle = show_indicator(receiver, true, false, false);
tokio::time::sleep(std::time::Duration::from_millis(1200)).await;
drop(sender);
let summary = tokio::time::timeout(std::time::Duration::from_secs(5), handle)
.await
.expect("indicator should complete within timeout")
.expect("indicator task should not panic");
assert_eq!(summary.total_delete_count, 1);
assert_eq!(summary.total_delete_bytes, 256);
assert_eq!(summary.total_error_count, 1);
assert_eq!(summary.total_skip_count, 1);
}
}