queue_workers 0.5.1

A Redis-backed job queue system for Rust applications
Documentation
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use uuid::Uuid;

#[async_trait]
pub trait Job: Send + Sync {
    type Output;
    type Error;

    async fn execute(&self) -> Result<Self::Output, Self::Error>;

    fn should_retry(&self, _error: &Self::Error, _attempt: u32) -> bool {
        true
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum JobStatus {
    Pending,
    Running,
    Completed,
    Failed(String),
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JobMetadata<T>
where
    T: Serialize,
{
    id: Uuid,
    payload: T,
    created_at: chrono::DateTime<chrono::Utc>,
    status: JobStatus,
}

impl<T> JobMetadata<T>
where
    T: Serialize + for<'de> Deserialize<'de> + Send + Sync,
{
    pub fn new(payload: T) -> Self {
        Self {
            id: Uuid::new_v4(),
            payload,
            created_at: chrono::Utc::now(),
            status: JobStatus::Pending,
        }
    }

    pub fn payload(&self) -> &T {
        &self.payload
    }

    pub fn payload_mut(&mut self) -> &mut T {
        &mut self.payload
    }

    pub fn update_status(&mut self, status: JobStatus) {
        self.status = status;
    }

    pub fn id(&self) -> Uuid {
        self.id
    }

    pub fn created_at(&self) -> chrono::DateTime<chrono::Utc> {
        self.created_at
    }

    pub fn status(&self) -> &JobStatus {
        &self.status
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[derive(Debug, Clone, Serialize, Deserialize)]
    struct TestPayload {
        data: String,
    }

    #[test]
    fn test_new_job_metadata() {
        let metadata = JobMetadata::new(TestPayload {
            data: "test".to_string(),
        });

        assert!(!metadata.id().is_nil());

        match metadata.status() {
            JobStatus::Pending => (),
            _ => panic!("New job should have Pending status"),
        }

        let now = chrono::Utc::now();
        let diff = now - metadata.created_at();
        assert!(diff.num_milliseconds() >= 0);
        assert!(diff.num_seconds() < 1);
    }

    #[test]
    fn test_update_status() {
        let mut metadata = JobMetadata::new(TestPayload {
            data: "test".to_string(),
        });

        metadata.update_status(JobStatus::Running);
        match metadata.status() {
            JobStatus::Running => (),
            _ => panic!("Status should be Running"),
        }

        metadata.update_status(JobStatus::Completed);
        match metadata.status() {
            JobStatus::Completed => (),
            _ => panic!("Status should be Completed"),
        }

        metadata.update_status(JobStatus::Failed("Test error".to_string()));
        match metadata.status() {
            JobStatus::Failed(msg) => assert_eq!(msg, "Test error"),
            _ => panic!("Status should be Failed"),
        }
    }

    #[test]
    fn test_clone() {
        let original = JobMetadata::new(TestPayload {
            data: "test".to_string(),
        });
        let cloned = original.clone();

        assert_eq!(original.id(), cloned.id());
        assert_eq!(original.created_at(), cloned.created_at());

        match (original.status(), cloned.status()) {
            (JobStatus::Pending, JobStatus::Pending) => (),
            _ => panic!("Cloned status should match original"),
        }
    }

    #[test]
    fn test_serialization() {
        let metadata = JobMetadata::new(TestPayload {
            data: "test".to_string(),
        });

        let serialized = serde_json::to_string(&metadata).expect("Failed to serialize");

        let deserialized: JobMetadata<TestPayload> =
            serde_json::from_str(&serialized).expect("Failed to deserialize");

        assert_eq!(metadata.id(), deserialized.id());
        assert_eq!(metadata.created_at(), deserialized.created_at());

        match (metadata.status(), deserialized.status()) {
            (JobStatus::Pending, JobStatus::Pending) => (),
            _ => panic!("Deserialized status should match original"),
        }
    }
}