use std::sync::Arc;
use std::time::Duration;
use tokio::sync::Mutex;
use tracing::{debug, info, warn};
use crate::app::hash::Md5Hash;
use crate::app::models::FileInfo;
use crate::errors::{DownloadError, DownloadResult};
use super::config::ConfigPresets;
use super::state::QueueState;
use super::stats::StatsManager;
use super::types::{QueueStats, WorkInfo, WorkQueueConfig};
#[derive(Debug)]
pub struct WorkQueue {
config: WorkQueueConfig,
state: Arc<Mutex<QueueState>>,
stats_manager: StatsManager,
}
impl WorkQueue {
pub fn new() -> Self {
Self::with_config(ConfigPresets::production())
}
pub fn with_config(config: WorkQueueConfig) -> Self {
if let Err(e) = config.validate() {
panic!("Invalid queue configuration: {}", e);
}
Self {
config,
state: Arc::new(Mutex::new(QueueState::new())),
stats_manager: StatsManager::new(),
}
}
pub async fn add_work(&self, file_info: FileInfo) -> DownloadResult<bool> {
self.add_work_with_priority(file_info, self.config.default_priority)
.await
}
pub async fn add_work_with_priority(
&self,
file_info: FileInfo,
priority: u32,
) -> DownloadResult<bool> {
let work_info = WorkInfo::new(file_info, priority);
let work_id = *work_info.work_id();
let result = {
let mut state = self.state.lock().await;
if state.is_completed(&work_id) {
state.get_stats_mut().duplicate_count += 1;
debug!("Skipping duplicate completed work: {}", work_id);
let stats = state.get_stats().clone();
drop(state); self.stats_manager.update_stats(stats).await;
return Ok(false);
}
if state.contains_work(&work_id) {
state.get_stats_mut().duplicate_count += 1;
debug!("Skipping duplicate existing work: {}", work_id);
let stats = state.get_stats().clone();
drop(state); self.stats_manager.update_stats(stats).await;
return Ok(false);
}
state.add_work(work_info)
};
match result {
Ok(()) => {
self.update_stats_from_state().await;
debug!("Added work to queue: {} (priority: {})", work_id, priority);
Ok(true)
}
Err(e) => Err(DownloadError::QueueError { message: e }),
}
}
pub async fn add_work_bulk(&self, file_infos: Vec<FileInfo>) -> DownloadResult<usize> {
let total_files = file_infos.len();
info!("Starting bulk add of {} files to queue", total_files);
let work_items: Vec<WorkInfo> = file_infos
.into_iter()
.map(|file_info| WorkInfo::new(file_info, self.config.default_priority))
.collect();
let (added_count, skipped_duplicates) = {
let mut state = self.state.lock().await;
let (added, duplicates) = state.add_work_bulk(work_items);
state.update_stats();
(added, duplicates)
};
self.update_stats_from_state().await;
info!(
"Bulk add completed: {}/{} files added, {} duplicates skipped",
added_count, total_files, skipped_duplicates
);
Ok(added_count)
}
pub async fn get_next_work(&self) -> Option<WorkInfo> {
let lock_start = std::time::Instant::now();
let mut state = self.state.lock().await;
let lock_duration = lock_start.elapsed();
if lock_duration > Duration::from_millis(10) {
debug!(
"Queue lock contention detected: {:?} wait time",
lock_duration
);
}
if let Some(work_id) = state.get_next_pending() {
if let Some(work_info) = state.get_work(&work_id) {
if work_info.is_available(self.config.retry_delay) {
return self.claim_work(&mut state, work_id).await;
}
}
}
let retry_candidates = state.find_retry_candidates(self.config.retry_delay);
if let Some(work_id) = retry_candidates.first() {
return self.claim_work(&mut state, *work_id).await;
}
let total_lock_time = lock_start.elapsed();
if total_lock_time > Duration::from_millis(5) {
debug!("No work found after {:?} lock time", total_lock_time);
}
None
}
async fn claim_work(&self, state: &mut QueueState, work_id: Md5Hash) -> Option<WorkInfo> {
let worker_id = state.next_worker_id();
if let Some(work_info) = state.get_work_mut(&work_id) {
let mut claimed_work = work_info.clone();
claimed_work.mark_in_progress(worker_id);
work_info.mark_in_progress(worker_id);
debug!("Claimed work {} for worker {}", work_id, worker_id);
state.update_stats();
let stats = state.get_stats().clone();
let _ = state;
self.stats_manager.update_stats(stats).await;
Some(claimed_work)
} else {
None
}
}
pub async fn mark_completed(&self, work_id: &Md5Hash) -> DownloadResult<()> {
let result = {
let mut state = self.state.lock().await;
state.mark_completed(work_id)
};
match result {
Ok(()) => {
self.update_stats_from_state().await;
info!("Marked work completed: {}", work_id);
Ok(())
}
Err(e) => Err(DownloadError::QueueError { message: e }),
}
}
pub async fn mark_failed(&self, work_id: &Md5Hash, error: &str) -> DownloadResult<()> {
let result = {
let mut state = self.state.lock().await;
state.mark_failed(work_id, error, self.config.max_retries)
};
match result {
Ok(()) => {
self.update_stats_from_state().await;
warn!("Marked work failed: {} - {}", work_id, error);
Ok(())
}
Err(e) => Err(DownloadError::QueueError { message: e }),
}
}
pub async fn stats(&self) -> QueueStats {
self.stats_manager.get_stats().await
}
pub async fn has_work_available(&self) -> bool {
let state = self.state.lock().await;
state.has_work_available(self.config.retry_delay)
}
pub async fn is_finished(&self) -> bool {
let state = self.state.lock().await;
state.is_finished(self.config.retry_delay)
}
pub async fn get_work_info(&self, work_id: &Md5Hash) -> Option<WorkInfo> {
let state = self.state.lock().await;
state.get_work(work_id).cloned()
}
pub async fn get_all_work(&self) -> Vec<WorkInfo> {
let state = self.state.lock().await;
state.get_all_work()
}
pub async fn cleanup(&self) -> usize {
let removed = {
let mut state = self.state.lock().await;
let removed = state.cleanup();
if removed > 0 {
state.update_stats();
}
removed
};
if removed > 0 {
self.update_stats_from_state().await;
info!("Cleaned up {} completed/abandoned work items", removed);
}
removed
}
pub async fn reset(&self) {
{
let mut state = self.state.lock().await;
state.reset();
}
self.stats_manager.reset().await;
info!("Reset work queue");
}
pub async fn handle_timeouts(&self) -> usize {
let timed_out = {
let mut state = self.state.lock().await;
let timeout_candidates = state.find_timed_out_work(self.config.work_timeout);
let mut count = 0;
for work_id in timeout_candidates {
if let Err(e) =
state.mark_failed(&work_id, "Worker timeout", self.config.max_retries)
{
warn!("Failed to mark work as timed out: {}", e);
} else {
count += 1;
warn!("Work item timed out: {}", work_id);
}
}
if count > 0 {
state.update_stats();
info!("Handled {} timed-out work items", count);
}
count
};
if timed_out > 0 {
self.update_stats_from_state().await;
}
timed_out
}
pub async fn has_capacity(&self) -> bool {
let stats = self.stats().await;
let total_active = stats.pending_count + stats.in_progress_count;
total_active < self.config.max_pending_items as u64
}
pub fn config(&self) -> &WorkQueueConfig {
&self.config
}
async fn update_stats_from_state(&self) {
let stats = {
let mut state = self.state.lock().await;
state.update_stats();
state.get_stats().clone()
};
self.stats_manager.update_stats(stats).await;
}
}
impl Default for WorkQueue {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::app::models::FileInfo;
use std::time::Duration;
use tempfile::TempDir;
use tokio::time::sleep;
fn create_test_file_info(hash: &str, station: &str) -> FileInfo {
let temp_dir = TempDir::new().unwrap();
let valid_hash = format!("{:0<32}", hash);
let hash_obj = Md5Hash::from_hex(&valid_hash).unwrap();
let path = format!(
"./data/uk-daily-temperature-obs/dataset-version-202407/devon/{}_twist/qc-version-1/midas-open_uk-daily-temperature-obs_dv-202407_devon_{}_twist_qcv-1_1980.csv",
station, station
);
FileInfo::new(hash_obj, path, temp_dir.path(), None).unwrap()
}
#[tokio::test]
async fn test_basic_queue_operations() {
let queue = WorkQueue::new();
let file_info = create_test_file_info("1", "01381");
let file_hash = file_info.hash;
let added = queue.add_work(file_info).await.unwrap();
assert!(added);
let stats = queue.stats().await;
assert_eq!(stats.total_added, 1);
assert_eq!(stats.pending_count, 1);
let work = queue.get_next_work().await.unwrap();
assert_eq!(work.work_id(), &file_hash);
assert!(work.is_in_progress());
queue.mark_completed(&file_hash).await.unwrap();
let stats = queue.stats().await;
assert_eq!(stats.completed_count, 1);
assert_eq!(stats.in_progress_count, 0);
}
#[tokio::test]
async fn test_duplicate_detection() {
let queue = WorkQueue::new();
let file_info = create_test_file_info("1", "01381");
let added1 = queue.add_work(file_info.clone()).await.unwrap();
let added2 = queue.add_work(file_info).await.unwrap();
assert!(added1);
assert!(!added2);
let stats = queue.stats().await;
assert_eq!(stats.total_added, 1);
assert_eq!(stats.duplicate_count, 1);
}
#[tokio::test]
async fn test_retry_mechanism() {
let config = super::super::config::ConfigPresets::testing();
let queue = WorkQueue::with_config(config);
let file_info = create_test_file_info("1", "01381");
let file_hash = file_info.hash;
queue.add_work(file_info).await.unwrap();
let work = queue.get_next_work().await.unwrap();
assert_eq!(work.work_id(), &file_hash);
queue.mark_failed(&file_hash, "Test error").await.unwrap();
assert!(queue.get_next_work().await.is_none());
sleep(Duration::from_millis(20)).await;
let retry_work = queue.get_next_work().await.unwrap();
assert_eq!(retry_work.work_id(), &file_hash);
assert!(retry_work.is_in_progress());
}
#[tokio::test]
async fn test_work_abandonment() {
let config = super::super::config::ConfigPresets::testing();
let queue = WorkQueue::with_config(config);
let file_info = create_test_file_info("1", "01381");
let file_hash = file_info.hash;
queue.add_work(file_info).await.unwrap();
for i in 0..2 {
if let Some(work) = queue.get_next_work().await {
queue
.mark_failed(work.work_id(), &format!("Error {}", i + 1))
.await
.unwrap();
sleep(Duration::from_millis(15)).await;
} else {
break;
}
}
let work_info = queue.get_work_info(&file_hash).await.unwrap();
assert!(work_info.is_abandoned());
let stats = queue.stats().await;
assert_eq!(stats.abandoned_count, 1);
}
#[tokio::test]
async fn test_bulk_operations() {
let queue = WorkQueue::new();
let file_infos = vec![
create_test_file_info("1", "01381"),
create_test_file_info("2", "01382"),
create_test_file_info("3", "01383"),
];
let added_count = queue.add_work_bulk(file_infos).await.unwrap();
assert_eq!(added_count, 3);
let stats = queue.stats().await;
assert_eq!(stats.total_added, 3);
assert_eq!(stats.pending_count, 3);
}
#[tokio::test]
async fn test_queue_capacity() {
let queue = WorkQueue::new();
assert!(queue.has_capacity().await);
let file_info = create_test_file_info("1", "01381");
queue.add_work(file_info).await.unwrap();
assert!(queue.has_capacity().await);
}
#[tokio::test]
async fn test_queue_cleanup() {
let queue = WorkQueue::new();
let file_info = create_test_file_info("1", "01381");
let _file_hash = file_info.hash;
queue.add_work(file_info).await.unwrap();
let work = queue.get_next_work().await.unwrap();
queue.mark_completed(work.work_id()).await.unwrap();
let removed = queue.cleanup().await;
assert_eq!(removed, 1);
let all_work = queue.get_all_work().await;
assert!(all_work.is_empty());
}
#[tokio::test]
async fn test_queue_reset() {
let queue = WorkQueue::new();
let file_info = create_test_file_info("1", "01381");
queue.add_work(file_info).await.unwrap();
queue.reset().await;
let stats = queue.stats().await;
assert_eq!(stats.total_added, 0);
assert_eq!(stats.pending_count, 0);
assert!(queue.is_finished().await);
}
#[tokio::test]
async fn test_concurrent_access() {
let queue = Arc::new(WorkQueue::new());
let mut handles = Vec::new();
for i in 1..=5 {
let queue_clone = Arc::clone(&queue);
let handle = tokio::spawn(async move {
let file_info = create_test_file_info(&format!("{}", i), &format!("0138{}", i));
queue_clone.add_work(file_info).await
});
handles.push(handle);
}
for handle in handles {
let result = handle.await.unwrap();
assert!(result.is_ok());
}
let stats = queue.stats().await;
assert_eq!(stats.total_added, 5);
assert_eq!(stats.pending_count, 5);
}
}