doitlater 0.5.0

A simple Redis based background jobs queue.
Documentation
use crate::{job::Job, Executable, Result};
use redis::{Client, Commands, Connection, LposOptions};
use std::env;
use std::time::Duration;

pub struct Queue {
    qname: String,
    client: Client,
    redis_conn: Connection,
}

impl Queue {
    pub fn new(name: &str, redis_connection_string: &str) -> Result<Self> {
        let client = Client::open(redis_connection_string)?;
        let conn = client.get_connection()?;
        Ok(Queue {
            qname: name.to_string(),
            redis_conn: conn,
            client,
        })
    }

    pub fn from_client(name: &str, client: Client) -> Result<Self> {
        let conn = client.get_connection()?;
        Ok(Queue {
            qname: name.to_string(),
            redis_conn: conn,
            client,
        })
    }

    pub fn new_from_env() -> Result<Self> {
        Self::new(
            &env::var("QUEUE_NAME").expect("QUEUE_NAME should be set"),
            &env::var("REDIS_URL").expect("REDIS_URL should be set"),
        )
    }

    fn jobs_storage_key(&self) -> String {
        format!("dil.{}.jobs", self.qname)
    }

    fn immediately_executable_key(&self) -> String {
        format!("dil.{}.executable", self.qname)
    }

    pub fn enqueue(&mut self, name: &str, executable: Box<dyn Executable>) -> Result<()> {
        let pos: Option<isize> = self.redis_conn.lpos(
            self.immediately_executable_key(),
            name,
            LposOptions::default(),
        )?;
        if pos.is_none() {
            let job = Job::new(name, executable);
            redis::pipe()
                .atomic()
                .rpush(self.immediately_executable_key(), name)
                .ignore()
                .hset(self.jobs_storage_key(), name, job.serialize()?)
                .ignore()
                .query::<()>(&mut self.redis_conn)?;
        }
        Ok(())
    }

    pub fn dequeue_executable_job(&mut self, timeout: Duration) -> Result<Option<Job>> {
        let result: Option<(String, String)> = self
            .redis_conn
            .blpop(self.immediately_executable_key(), timeout.as_secs_f64())?;
        if let Some((_key, name)) = result {
            let data: Vec<u8> = self.redis_conn.hget(self.jobs_storage_key(), name)?;
            Ok(Some(Job::from_serialized(&data)?))
        } else {
            Ok(None)
        }
    }

    pub fn delete_job_data(&mut self, name: &str) -> Result<()> {
        self.redis_conn
            .hdel::<_, _, ()>(self.jobs_storage_key(), name)?;
        Ok(())
    }

    pub fn name(&self) -> &str {
        &self.qname
    }

    pub fn get_client_clone(&self) -> Client {
        self.client.clone()
    }
}