folk-plugin-jobs 0.3.3

Queue consumer plugin for Folk — pulls jobs from memory or Redis and dispatches to PHP workers
Documentation
use std::fmt;
use std::time::Duration;

use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum DriverKind {
    Memory,
    Redis,
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum BackoffStrategy {
    /// delay * 2^retries
    Exponential,
    /// delay * (retries + 1)
    Linear,
    /// constant delay
    Fixed,
}

impl BackoffStrategy {
    pub fn delay(&self, base: Duration, attempt: u32) -> Duration {
        match self {
            Self::Exponential => base.saturating_mul(1u32 << attempt.min(10)),
            Self::Linear => base.saturating_mul(attempt + 1),
            Self::Fixed => base,
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct QueueConfig {
    pub name: String,
    pub concurrency: usize,
    pub max_retries: u32,

    /// Base retry delay (e.g. "1s"). Default: 1s
    #[serde(with = "humantime_serde")]
    pub retry_delay: Duration,

    /// Backoff strategy: exponential | linear | fixed. Default: exponential
    pub retry_backoff: BackoffStrategy,

    /// Job execution timeout (e.g. "60s"). 0 = no timeout. Default: 60s
    #[serde(with = "humantime_serde")]
    pub job_timeout: Duration,

    /// Queue name for failed jobs after max retries. None = discard.
    pub dead_letter_queue: Option<String>,

    /// Queue priority (lower = higher priority). Default: 10
    pub priority: u32,
}

impl Default for QueueConfig {
    fn default() -> Self {
        Self {
            name: "default".into(),
            concurrency: 4,
            max_retries: 3,
            retry_delay: Duration::from_secs(1),
            retry_backoff: BackoffStrategy::Exponential,
            job_timeout: Duration::from_secs(60),
            dead_letter_queue: None,
            priority: 10,
        }
    }
}

#[derive(Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct JobsConfig {
    pub driver: DriverKind,
    pub host: String,
    pub port: u16,
    pub password: String,
    pub db: u32,
    pub queues: Vec<QueueConfig>,
}

impl fmt::Debug for JobsConfig {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("JobsConfig")
            .field("driver", &self.driver)
            .field("host", &self.host)
            .field("port", &self.port)
            .field("password", &"[REDACTED]")
            .field("db", &self.db)
            .field("queues", &self.queues)
            .finish()
    }
}

impl JobsConfig {
    /// Build a Redis connection URL from the unified fields.
    pub fn redis_url(&self) -> String {
        let auth = if self.password.is_empty() {
            String::new()
        } else {
            format!(":{}@", self.password)
        };
        format!("redis://{}{}:{}/{}", auth, self.host, self.port, self.db)
    }
}

impl Default for JobsConfig {
    fn default() -> Self {
        Self {
            driver: DriverKind::Memory,
            host: "127.0.0.1".into(),
            port: 6379,
            password: String::new(),
            db: 0,
            queues: vec![QueueConfig::default()],
        }
    }
}