cloudiful-scheduler 0.3.5

Single-job async scheduling library for background work with optional Valkey-backed state.
Documentation
use std::error::Error;
use std::fmt::{self, Display, Formatter};
use tokio::task::JoinError;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InvalidJobKind {
    ZeroInterval,
    IntervalOutOfRange,
    CronExpression,
    Other,
}

#[derive(Debug)]
pub struct InvalidJobError {
    kind: InvalidJobKind,
    message: String,
}

impl InvalidJobError {
    pub fn kind(&self) -> InvalidJobKind {
        self.kind
    }

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

impl Display for InvalidJobError {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        f.write_str(&self.message)
    }
}

impl Error for InvalidJobError {}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StoreErrorKind {
    Connection,
    Data,
    Unknown,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExecutionGuardErrorKind {
    Connection,
    Data,
    Unknown,
}

#[derive(Debug)]
pub struct StoreError {
    kind: StoreErrorKind,
    source: Box<dyn Error + Send + Sync>,
}

impl StoreError {
    pub fn new<E>(source: E, kind: StoreErrorKind) -> Self
    where
        E: Error + Send + Sync + 'static,
    {
        Self {
            kind,
            source: Box::new(source),
        }
    }

    pub fn kind(&self) -> StoreErrorKind {
        self.kind
    }
}

impl Display for StoreError {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.source)
    }
}

impl Error for StoreError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        Some(self.source.as_ref())
    }
}

#[derive(Debug)]
pub struct ExecutionGuardError {
    kind: ExecutionGuardErrorKind,
    source: Box<dyn Error + Send + Sync>,
}

impl ExecutionGuardError {
    pub fn new<E>(source: E, kind: ExecutionGuardErrorKind) -> Self
    where
        E: Error + Send + Sync + 'static,
    {
        Self {
            kind,
            source: Box::new(source),
        }
    }

    pub fn kind(&self) -> ExecutionGuardErrorKind {
        self.kind
    }
}

impl Display for ExecutionGuardError {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.source)
    }
}

impl Error for ExecutionGuardError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        Some(self.source.as_ref())
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TaskJoinErrorKind {
    Cancelled,
    Panic,
    Unknown,
}

#[derive(Debug)]
pub struct TaskJoinError {
    kind: TaskJoinErrorKind,
    message: String,
}

impl TaskJoinError {
    pub fn kind(&self) -> TaskJoinErrorKind {
        self.kind
    }

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

impl Display for TaskJoinError {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        f.write_str(&self.message)
    }
}

impl Error for TaskJoinError {}

#[derive(Debug)]
pub enum SchedulerError {
    InvalidJob(InvalidJobError),
    Store(StoreError),
    ExecutionGuard(ExecutionGuardError),
    TaskJoin(TaskJoinError),
}

impl Display for SchedulerError {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        match self {
            SchedulerError::InvalidJob(message) => write!(f, "invalid job: {message}"),
            SchedulerError::Store(error) => write!(f, "state store error: {error}"),
            SchedulerError::ExecutionGuard(error) => {
                write!(f, "execution guard error: {error}")
            }
            SchedulerError::TaskJoin(message) => write!(f, "task join error: {message}"),
        }
    }
}

impl Error for SchedulerError {}

impl SchedulerError {
    pub(crate) fn invalid_zero_interval() -> Self {
        Self::invalid_job_with_kind(
            InvalidJobKind::ZeroInterval,
            "interval schedule must be greater than zero",
        )
    }

    pub(crate) fn invalid_interval_out_of_range() -> Self {
        Self::invalid_job_with_kind(
            InvalidJobKind::IntervalOutOfRange,
            "interval schedule is too large to represent",
        )
    }

    pub(crate) fn invalid_cron(message: impl Into<String>) -> Self {
        Self::invalid_job_with_kind(InvalidJobKind::CronExpression, message)
    }

    pub(crate) fn invalid_job_with_kind(kind: InvalidJobKind, message: impl Into<String>) -> Self {
        Self::InvalidJob(InvalidJobError {
            kind,
            message: message.into(),
        })
    }

    pub(crate) fn store<E>(error: E, kind: StoreErrorKind) -> Self
    where
        E: Error + Send + Sync + 'static,
    {
        Self::Store(StoreError::new(error, kind))
    }

    pub(crate) fn task_join(error: JoinError) -> Self {
        let kind = if error.is_cancelled() {
            TaskJoinErrorKind::Cancelled
        } else if error.is_panic() {
            TaskJoinErrorKind::Panic
        } else {
            TaskJoinErrorKind::Unknown
        };

        Self::TaskJoin(TaskJoinError {
            kind,
            message: error.to_string(),
        })
    }

    pub(crate) fn execution_guard<E>(error: E, kind: ExecutionGuardErrorKind) -> Self
    where
        E: Error + Send + Sync + 'static,
    {
        Self::ExecutionGuard(ExecutionGuardError::new(error, kind))
    }
}