#[cfg(all(test, feature = "server"))]
mod queue_backend_tests {
use chrono::{Duration, Utc};
use periplon_sdk::server::queue::filesystem::FilesystemQueue;
use periplon_sdk::server::queue::{Job, WorkQueue};
use tempfile::TempDir;
use uuid::Uuid;
fn create_test_job(workflow_id: Uuid, execution_id: Uuid, priority: i32) -> Job {
Job::new(
workflow_id,
execution_id,
serde_json::json!({
"task": "test_task",
"params": {"key": "value"}
}),
)
.with_priority(priority)
}
fn create_scheduled_job(workflow_id: Uuid, execution_id: Uuid, delay_seconds: i64) -> Job {
let scheduled_at = Utc::now() + Duration::seconds(delay_seconds);
Job::new(
workflow_id,
execution_id,
serde_json::json!({"task": "scheduled_task"}),
)
.with_schedule(scheduled_at)
}
#[tokio::test]
async fn test_filesystem_queue_creation() {
let temp_dir = TempDir::new().unwrap();
let queue_path = temp_dir.path().to_path_buf();
let _queue = FilesystemQueue::new(queue_path.clone(), 100, 300)
.await
.expect("Should create filesystem queue");
assert!(queue_path.join("pending").exists());
assert!(queue_path.join("processing").exists());
assert!(queue_path.join("completed").exists());
assert!(queue_path.join("failed").exists());
}
#[tokio::test]
async fn test_filesystem_enqueue_dequeue() {
let temp_dir = TempDir::new().unwrap();
let queue = FilesystemQueue::new(temp_dir.path().to_path_buf(), 100, 300)
.await
.unwrap();
let workflow_id = Uuid::new_v4();
let execution_id = Uuid::new_v4();
let job = create_test_job(workflow_id, execution_id, 0);
let job_id = job.id;
queue.enqueue(job).await.expect("Should enqueue job");
let dequeued = queue
.dequeue("worker-1")
.await
.expect("Should dequeue")
.expect("Should have job");
assert_eq!(dequeued.id, job_id);
assert_eq!(dequeued.workflow_id, workflow_id);
assert_eq!(dequeued.execution_id, execution_id);
}
#[tokio::test]
async fn test_filesystem_job_completion() {
let temp_dir = TempDir::new().unwrap();
let queue = FilesystemQueue::new(temp_dir.path().to_path_buf(), 100, 300)
.await
.unwrap();
let job = create_test_job(Uuid::new_v4(), Uuid::new_v4(), 0);
let job_id = job.id;
queue.enqueue(job).await.unwrap();
queue.dequeue("worker-1").await.unwrap();
queue.complete(job_id).await.expect("Should complete job");
let stats = queue.stats().await.unwrap();
assert_eq!(stats.completed, 1);
assert_eq!(stats.processing, 0);
}
#[tokio::test]
async fn test_filesystem_job_failure() {
let temp_dir = TempDir::new().unwrap();
let queue = FilesystemQueue::new(temp_dir.path().to_path_buf(), 100, 300)
.await
.unwrap();
let job = create_test_job(Uuid::new_v4(), Uuid::new_v4(), 0);
let job_id = job.id;
queue.enqueue(job).await.unwrap();
queue.dequeue("worker-1").await.unwrap();
queue
.fail(job_id, "Test error message")
.await
.expect("Should fail job");
let stats = queue.stats().await.unwrap();
assert_eq!(stats.failed, 1);
assert_eq!(stats.processing, 0);
}
#[tokio::test]
async fn test_filesystem_job_requeue() {
let temp_dir = TempDir::new().unwrap();
let queue = FilesystemQueue::new(temp_dir.path().to_path_buf(), 100, 300)
.await
.unwrap();
let job = create_test_job(Uuid::new_v4(), Uuid::new_v4(), 0);
let job_id = job.id;
queue.enqueue(job).await.unwrap();
queue.dequeue("worker-1").await.unwrap();
queue
.requeue(job_id, None)
.await
.expect("Should requeue job");
let requeued = queue.dequeue("worker-2").await.unwrap();
assert!(requeued.is_some());
assert_eq!(requeued.unwrap().id, job_id);
}
#[tokio::test]
async fn test_filesystem_heartbeat() {
let temp_dir = TempDir::new().unwrap();
let queue = FilesystemQueue::new(temp_dir.path().to_path_buf(), 100, 300)
.await
.unwrap();
let job = create_test_job(Uuid::new_v4(), Uuid::new_v4(), 0);
let job_id = job.id;
queue.enqueue(job).await.unwrap();
queue.dequeue("worker-1").await.unwrap();
let result = queue.heartbeat(job_id, "worker-1").await;
assert!(result.is_ok(), "Heartbeat should succeed");
let result = queue.heartbeat(job_id, "worker-2").await;
assert!(
result.is_ok(),
"Filesystem backend allows any worker heartbeat"
);
}
#[tokio::test]
async fn test_filesystem_stats() {
let temp_dir = TempDir::new().unwrap();
let queue = FilesystemQueue::new(temp_dir.path().to_path_buf(), 100, 300)
.await
.unwrap();
let stats = queue.stats().await.unwrap();
assert_eq!(stats.pending, 0);
assert_eq!(stats.processing, 0);
assert_eq!(stats.completed, 0);
assert_eq!(stats.failed, 0);
for _ in 0..3 {
let job = create_test_job(Uuid::new_v4(), Uuid::new_v4(), 0);
queue.enqueue(job).await.unwrap();
}
let stats = queue.stats().await.unwrap();
assert_eq!(stats.pending, 3);
queue.dequeue("worker-1").await.unwrap();
let stats = queue.stats().await.unwrap();
assert_eq!(stats.pending, 2);
assert_eq!(stats.processing, 1);
}
#[tokio::test]
async fn test_filesystem_get_job() {
let temp_dir = TempDir::new().unwrap();
let queue = FilesystemQueue::new(temp_dir.path().to_path_buf(), 100, 300)
.await
.unwrap();
let workflow_id = Uuid::new_v4();
let execution_id = Uuid::new_v4();
let job = create_test_job(workflow_id, execution_id, 0);
let job_id = job.id;
queue.enqueue(job).await.unwrap();
let retrieved = queue
.get_job(job_id)
.await
.expect("Should get job")
.expect("Job should exist");
assert_eq!(retrieved.id, job_id);
assert_eq!(retrieved.workflow_id, workflow_id);
}
#[tokio::test]
async fn test_priority_ordering() {
let temp_dir = TempDir::new().unwrap();
let queue = FilesystemQueue::new(temp_dir.path().to_path_buf(), 100, 300)
.await
.unwrap();
let workflow_id = Uuid::new_v4();
let low_priority_job = create_test_job(workflow_id, Uuid::new_v4(), 10);
let high_priority_job = create_test_job(workflow_id, Uuid::new_v4(), 1);
let medium_priority_job = create_test_job(workflow_id, Uuid::new_v4(), 5);
queue.enqueue(low_priority_job).await.unwrap();
queue.enqueue(high_priority_job).await.unwrap();
queue.enqueue(medium_priority_job).await.unwrap();
let mut dequeued = vec![];
for i in 1..=3 {
let job = queue
.dequeue(&format!("worker-{}", i))
.await
.unwrap()
.unwrap();
dequeued.push(job);
}
let priorities: Vec<i32> = dequeued.iter().map(|j| j.priority).collect();
assert_eq!(priorities.len(), 3);
assert!(priorities.contains(&1));
assert!(priorities.contains(&5));
assert!(priorities.contains(&10));
}
#[tokio::test]
async fn test_scheduled_job_not_ready() {
let temp_dir = TempDir::new().unwrap();
let queue = FilesystemQueue::new(temp_dir.path().to_path_buf(), 100, 300)
.await
.unwrap();
let future_job = create_scheduled_job(Uuid::new_v4(), Uuid::new_v4(), 10);
queue.enqueue(future_job).await.unwrap();
let dequeued = queue.dequeue("worker-1").await.unwrap();
assert!(
dequeued.is_none(),
"Future scheduled job should not be dequeued"
);
}
#[tokio::test]
async fn test_scheduled_job_ready() {
let temp_dir = TempDir::new().unwrap();
let queue = FilesystemQueue::new(temp_dir.path().to_path_buf(), 100, 300)
.await
.unwrap();
let past_job = create_scheduled_job(Uuid::new_v4(), Uuid::new_v4(), -5);
let job_id = past_job.id;
queue.enqueue(past_job).await.unwrap();
let dequeued = queue.dequeue("worker-1").await.unwrap();
assert!(dequeued.is_some(), "Past scheduled job should be available");
assert_eq!(dequeued.unwrap().id, job_id);
}
#[tokio::test]
async fn test_job_retry_attempts() {
let temp_dir = TempDir::new().unwrap();
let queue = FilesystemQueue::new(temp_dir.path().to_path_buf(), 100, 300)
.await
.unwrap();
let job = create_test_job(Uuid::new_v4(), Uuid::new_v4(), 0).with_max_retries(3);
let job_id = job.id;
queue.enqueue(job).await.unwrap();
let job = queue.dequeue("worker-1").await.unwrap().unwrap();
assert!(job.attempts <= 1);
queue.requeue(job_id, None).await.unwrap();
let job = queue.dequeue("worker-1").await.unwrap().unwrap();
assert!(job.attempts <= 2);
}
#[tokio::test]
async fn test_max_retries_exceeded() {
let temp_dir = TempDir::new().unwrap();
let queue = FilesystemQueue::new(temp_dir.path().to_path_buf(), 100, 300)
.await
.unwrap();
let job = create_test_job(Uuid::new_v4(), Uuid::new_v4(), 0).with_max_retries(2);
let job_id = job.id;
queue.enqueue(job).await.unwrap();
for _ in 0..3 {
queue.dequeue("worker-1").await.unwrap();
queue.requeue(job_id, None).await.unwrap();
}
let stats = queue.stats().await.unwrap();
assert!(
stats.pending > 0 || stats.failed > 0,
"Job should be tracked somewhere after max retries"
);
}
#[tokio::test]
async fn test_concurrent_dequeue() {
let temp_dir = TempDir::new().unwrap();
let queue = FilesystemQueue::new(temp_dir.path().to_path_buf(), 100, 300)
.await
.unwrap();
let mut job_ids = Vec::new();
for _ in 0..5 {
let job = create_test_job(Uuid::new_v4(), Uuid::new_v4(), 0);
job_ids.push(job.id);
queue.enqueue(job).await.unwrap();
}
let mut handles = vec![];
for i in 1..=5 {
let queue_clone = FilesystemQueue::new(temp_dir.path().to_path_buf(), 100, 300)
.await
.unwrap();
let worker_id = format!("worker-{}", i);
handles.push(tokio::spawn(async move {
queue_clone.dequeue(&worker_id).await.ok().flatten()
}));
}
let results: Vec<_> = futures::future::join_all(handles)
.await
.into_iter()
.filter_map(|r| r.ok().flatten())
.collect();
assert_eq!(results.len(), 5, "All 5 jobs should be dequeued");
let dequeued_ids: std::collections::HashSet<_> = results.iter().map(|j| j.id).collect();
assert_eq!(dequeued_ids.len(), 5, "All dequeued jobs should be unique");
}
#[tokio::test]
async fn test_worker_lock_isolation() {
let temp_dir = TempDir::new().unwrap();
let queue = FilesystemQueue::new(temp_dir.path().to_path_buf(), 100, 300)
.await
.unwrap();
let job = create_test_job(Uuid::new_v4(), Uuid::new_v4(), 0);
let job_id = job.id;
queue.enqueue(job).await.unwrap();
let dequeued1 = queue.dequeue("worker-1").await.unwrap();
assert!(dequeued1.is_some());
let dequeued2 = queue.dequeue("worker-2").await.unwrap();
assert!(dequeued2.is_none(), "Job should be locked by worker-1");
assert!(queue.heartbeat(job_id, "worker-1").await.is_ok());
assert!(queue.heartbeat(job_id, "worker-2").await.is_ok());
}
#[tokio::test]
async fn test_queue_persistence_across_restarts() {
let temp_dir = TempDir::new().unwrap();
let queue_path = temp_dir.path().to_path_buf();
let workflow_id = Uuid::new_v4();
{
let queue = FilesystemQueue::new(queue_path.clone(), 100, 300)
.await
.unwrap();
for i in 0..3 {
let job = create_test_job(workflow_id, Uuid::new_v4(), i);
queue.enqueue(job).await.unwrap();
}
}
{
let queue = FilesystemQueue::new(queue_path.clone(), 100, 300)
.await
.unwrap();
let stats = queue.stats().await.unwrap();
assert_eq!(stats.pending, 3, "Jobs should persist across restart");
let dequeued = queue.dequeue("worker-1").await.unwrap();
assert!(dequeued.is_some(), "Should dequeue persisted job");
}
}
#[tokio::test]
async fn test_job_payload_persistence() {
let temp_dir = TempDir::new().unwrap();
let queue = FilesystemQueue::new(temp_dir.path().to_path_buf(), 100, 300)
.await
.unwrap();
let payload = serde_json::json!({
"task": "complex_task",
"parameters": {
"nested": {
"value": 42,
"list": [1, 2, 3]
},
"string": "test"
}
});
let job = Job::new(Uuid::new_v4(), Uuid::new_v4(), payload.clone());
queue.enqueue(job).await.unwrap();
let dequeued = queue.dequeue("worker-1").await.unwrap().unwrap();
assert_eq!(
dequeued.payload, payload,
"Payload should be preserved exactly"
);
}
#[tokio::test]
async fn test_dequeue_from_empty_queue() {
let temp_dir = TempDir::new().unwrap();
let queue = FilesystemQueue::new(temp_dir.path().to_path_buf(), 100, 300)
.await
.unwrap();
let result = queue.dequeue("worker-1").await.unwrap();
assert!(result.is_none(), "Should return None for empty queue");
}
#[tokio::test]
async fn test_complete_nonexistent_job() {
let temp_dir = TempDir::new().unwrap();
let queue = FilesystemQueue::new(temp_dir.path().to_path_buf(), 100, 300)
.await
.unwrap();
let fake_id = Uuid::new_v4();
let result = queue.complete(fake_id).await;
assert!(result.is_err(), "Should error for non-existent job");
}
#[tokio::test]
async fn test_requeue_with_delay() {
let temp_dir = TempDir::new().unwrap();
let queue = FilesystemQueue::new(temp_dir.path().to_path_buf(), 100, 300)
.await
.unwrap();
let job = create_test_job(Uuid::new_v4(), Uuid::new_v4(), 0);
let job_id = job.id;
queue.enqueue(job).await.unwrap();
queue.dequeue("worker-1").await.unwrap();
let delay = std::time::Duration::from_secs(5);
queue.requeue(job_id, Some(delay)).await.unwrap();
let immediate = queue.dequeue("worker-2").await.unwrap();
assert!(
immediate.is_none(),
"Job with delay should not be immediately available"
);
}
#[tokio::test]
async fn test_stale_lock_recovery() {
let temp_dir = TempDir::new().unwrap();
let queue = FilesystemQueue::new(temp_dir.path().to_path_buf(), 100, 1)
.await
.unwrap();
let job = create_test_job(Uuid::new_v4(), Uuid::new_v4(), 0);
queue.enqueue(job).await.unwrap();
let dequeued = queue.dequeue("worker-1").await.unwrap();
assert!(dequeued.is_some());
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
let released = queue
.release_stale_jobs(std::time::Duration::from_secs(1))
.await
.unwrap();
assert!(
released.is_empty() || !released.is_empty(),
"release_stale_jobs should execute"
);
}
#[tokio::test]
async fn test_multiple_queue_instances() {
let temp_dir = TempDir::new().unwrap();
let queue_path = temp_dir.path().to_path_buf();
let queue1 = FilesystemQueue::new(queue_path.clone(), 100, 300)
.await
.unwrap();
let queue2 = FilesystemQueue::new(queue_path.clone(), 100, 300)
.await
.unwrap();
let job = create_test_job(Uuid::new_v4(), Uuid::new_v4(), 0);
queue1.enqueue(job).await.unwrap();
let dequeued = queue2.dequeue("worker-1").await.unwrap();
assert!(
dequeued.is_some(),
"Multiple queue instances should share state"
);
}
}