use std::time::Duration;
use serde::{Serialize, Deserialize, de::DeserializeOwned};
use async_trait::async_trait;
use thiserror::Error;
use uuid::Uuid;
use chrono::{DateTime, Utc};
pub mod backends;
pub mod config;
pub mod worker;
pub mod scheduler;
pub use backends::*;
pub use config::*;
pub use worker::*;
pub use scheduler::*;
#[derive(Error, Debug)]
pub enum QueueError {
#[error("Serialization error: {0}")]
Serialization(#[from] serde_json::Error),
#[error("Backend error: {0}")]
Backend(String),
#[error("Job not found: {0}")]
JobNotFound(String),
#[error("Queue configuration error: {0}")]
Configuration(String),
#[error("Network error: {0}")]
Network(String),
#[error("Timeout error")]
Timeout,
#[error("Job execution failed: {0}")]
Execution(String),
}
pub type QueueResult<T> = Result<T, QueueError>;
pub type JobResult<T> = Result<T, Box<dyn std::error::Error + Send + Sync>>;
pub type JobId = Uuid;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum Priority {
Low = 0,
Normal = 1,
High = 2,
Critical = 3,
}
impl Default for Priority {
fn default() -> Self {
Priority::Normal
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum JobState {
Pending,
Processing,
Completed,
Failed,
Dead,
}
#[async_trait]
pub trait Job: Send + Sync + Serialize + DeserializeOwned {
async fn execute(&self) -> JobResult<()>;
fn job_type(&self) -> &'static str;
fn max_retries(&self) -> u32 {
3
}
fn retry_delay(&self, attempt: u32) -> Duration {
Duration::from_secs(1 << attempt.min(6)) }
fn timeout(&self) -> Duration {
Duration::from_secs(300)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JobEntry {
id: JobId,
job_type: String,
payload: serde_json::Value,
priority: Priority,
state: JobState,
attempts: u32,
max_retries: u32,
created_at: DateTime<Utc>,
run_at: DateTime<Utc>,
processed_at: Option<DateTime<Utc>>,
last_error: Option<String>,
}
impl JobEntry {
pub fn new<T: Job>(job: T, priority: Option<Priority>, delay: Option<Duration>) -> QueueResult<Self> {
let now = Utc::now();
let run_at = match delay {
Some(d) => now + chrono::Duration::from_std(d)
.map_err(|e| QueueError::Configuration(format!("Invalid delay duration: {}", e)))?,
None => now,
};
let job_type = job.job_type().to_string();
let max_retries = job.max_retries();
let payload = serde_json::to_value(job)?;
Ok(JobEntry {
id: Uuid::new_v4(),
job_type,
payload,
priority: priority.unwrap_or_default(),
state: JobState::Pending,
attempts: 0,
max_retries,
created_at: now,
run_at,
processed_at: None,
last_error: None,
})
}
pub fn id(&self) -> JobId {
self.id
}
pub fn job_type(&self) -> &str {
&self.job_type
}
pub fn priority(&self) -> Priority {
self.priority
}
pub fn state(&self) -> &JobState {
&self.state
}
pub fn attempts(&self) -> u32 {
self.attempts
}
pub fn run_at(&self) -> DateTime<Utc> {
self.run_at
}
pub fn payload(&self) -> &serde_json::Value {
&self.payload
}
pub fn is_ready(&self) -> bool {
matches!(self.state, JobState::Pending | JobState::Failed) && self.run_at <= Utc::now()
}
pub async fn execute<T: Job>(&self) -> JobResult<()> {
let job: T = serde_json::from_value(self.payload.clone())?;
job.execute().await
}
pub(crate) fn mark_processing(&mut self) {
self.state = JobState::Processing;
self.processed_at = Some(Utc::now());
}
pub(crate) fn mark_completed(&mut self) {
self.state = JobState::Completed;
}
pub(crate) fn mark_failed(&mut self, error: String) {
self.attempts += 1;
self.last_error = Some(error);
if self.attempts >= self.max_retries {
self.state = JobState::Dead;
} else {
self.state = JobState::Failed;
let delay = Duration::from_secs(1 << self.attempts.min(6));
let chrono_delay = chrono::Duration::from_std(delay)
.unwrap_or(chrono::Duration::MAX);
self.run_at = Utc::now() + chrono_delay;
}
}
pub(crate) fn reset_for_retry(&mut self) {
self.attempts = 0;
self.state = JobState::Pending;
self.run_at = Utc::now();
self.last_error = None;
self.processed_at = None;
}
pub(crate) fn new_with_job_type(
job_type: String,
payload: serde_json::Value,
priority: Option<Priority>,
delay: Option<Duration>,
max_retries: u32,
) -> QueueResult<Self> {
let now = Utc::now();
let run_at = delay.map(|d| now + chrono::Duration::from_std(d).unwrap()).unwrap_or(now);
Ok(JobEntry {
id: Uuid::new_v4(),
job_type,
payload,
priority: priority.unwrap_or_default(),
state: JobState::Pending,
attempts: 0,
max_retries,
created_at: now,
run_at,
processed_at: None,
last_error: None,
})
}
}
#[async_trait]
pub trait QueueBackend: Send + Sync {
async fn enqueue(&self, job: JobEntry) -> QueueResult<JobId>;
async fn dequeue(&self) -> QueueResult<Option<JobEntry>>;
async fn complete(&self, job_id: JobId, result: JobResult<()>) -> QueueResult<()>;
async fn get_job(&self, job_id: JobId) -> QueueResult<Option<JobEntry>>;
async fn get_jobs_by_state(&self, state: JobState, limit: Option<usize>) -> QueueResult<Vec<JobEntry>>;
async fn remove_job(&self, job_id: JobId) -> QueueResult<bool>;
async fn clear(&self) -> QueueResult<()>;
async fn stats(&self) -> QueueResult<QueueStats>;
async fn requeue_job(&self, job_id: JobId, mut job: JobEntry) -> QueueResult<bool> {
if self.remove_job(job_id).await? {
job.reset_for_retry();
self.enqueue(job).await?;
Ok(true)
} else {
Ok(false)
}
}
async fn clear_jobs_by_state(&self, state: JobState) -> QueueResult<u64> {
let jobs = self.get_jobs_by_state(state, None).await?;
let count = jobs.len() as u64;
for job in jobs {
self.remove_job(job.id()).await?;
}
Ok(count)
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct QueueStats {
pub pending_jobs: u64,
pub processing_jobs: u64,
pub completed_jobs: u64,
pub failed_jobs: u64,
pub dead_jobs: u64,
pub total_jobs: u64,
}
pub struct Queue<B: QueueBackend> {
backend: B,
}
impl<B: QueueBackend> Queue<B> {
pub fn new(backend: B) -> Self {
Self { backend }
}
pub async fn enqueue<T: Job>(&self, job: T, priority: Option<Priority>) -> QueueResult<JobId> {
let entry = JobEntry::new(job, priority, None)?;
self.backend.enqueue(entry).await
}
pub async fn enqueue_delayed<T: Job>(&self, job: T, delay: Duration, priority: Option<Priority>) -> QueueResult<JobId> {
let entry = JobEntry::new(job, priority, Some(delay))?;
self.backend.enqueue(entry).await
}
pub async fn dequeue(&self) -> QueueResult<Option<JobEntry>> {
self.backend.dequeue().await
}
pub async fn complete(&self, job_id: JobId, result: JobResult<()>) -> QueueResult<()> {
self.backend.complete(job_id, result).await
}
pub async fn get_job(&self, job_id: JobId) -> QueueResult<Option<JobEntry>> {
self.backend.get_job(job_id).await
}
pub async fn get_jobs_by_state(&self, state: JobState, limit: Option<usize>) -> QueueResult<Vec<JobEntry>> {
self.backend.get_jobs_by_state(state, limit).await
}
pub async fn remove_job(&self, job_id: JobId) -> QueueResult<bool> {
self.backend.remove_job(job_id).await
}
pub async fn clear(&self) -> QueueResult<()> {
self.backend.clear().await
}
pub async fn stats(&self) -> QueueResult<QueueStats> {
self.backend.stats().await
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::backends::MemoryBackend;
#[derive(Debug, Clone, Serialize, Deserialize)]
struct TestJob {
message: String,
}
#[async_trait]
impl Job for TestJob {
async fn execute(&self) -> JobResult<()> {
println!("Executing job: {}", self.message);
Ok(())
}
fn job_type(&self) -> &'static str {
"test"
}
}
#[tokio::test]
async fn test_job_entry_creation() {
let job = TestJob {
message: "Hello, World!".to_string(),
};
let entry = JobEntry::new(job, Some(Priority::High), None).unwrap();
assert_eq!(entry.job_type(), "test");
assert_eq!(entry.priority(), Priority::High);
assert_eq!(entry.state(), &JobState::Pending);
assert_eq!(entry.attempts(), 0);
assert!(entry.is_ready());
}
#[tokio::test]
async fn test_delayed_job() {
let job = TestJob {
message: "Delayed job".to_string(),
};
let delay = Duration::from_secs(60);
let entry = JobEntry::new(job, None, Some(delay)).unwrap();
assert!(!entry.is_ready());
assert!(entry.run_at() > Utc::now());
}
#[tokio::test]
async fn test_duration_conversion_error_handling() {
use std::time::Duration as StdDuration;
let job = TestJob {
message: "test".to_string(),
};
let max_delay = StdDuration::MAX;
let result = JobEntry::new(job.clone(), None, Some(max_delay));
assert!(result.is_err());
if let Err(QueueError::Configuration(msg)) = result {
assert!(msg.contains("Invalid delay duration"));
} else {
panic!("Expected Configuration error for invalid delay duration");
}
let entry = JobEntry::new(job, None, None).unwrap();
let mut job_entry = entry;
job_entry.attempts = 100;
job_entry.mark_failed("test error".to_string());
assert_eq!(job_entry.state, JobState::Dead); }
#[tokio::test]
async fn test_queue_basic_operations() {
let backend = MemoryBackend::new(QueueConfig::default());
let queue = Queue::new(backend);
let job = TestJob {
message: "Test job".to_string(),
};
let job_id = queue.enqueue(job, Some(Priority::Normal)).await.unwrap();
let job_entry = queue.dequeue().await.unwrap().unwrap();
assert_eq!(job_entry.id(), job_id);
assert_eq!(job_entry.job_type(), "test");
let result = job_entry.execute::<TestJob>().await;
queue.complete(job_id, result).await.unwrap();
let stats = queue.stats().await.unwrap();
assert_eq!(stats.total_jobs, 1);
assert_eq!(stats.completed_jobs, 1);
}
}