#[cfg(test)]
mod tests;
use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
use std::sync::mpsc::{self, Sender};
use std::sync::Arc;
use std::thread;
use std::time::Duration;
pub trait Job: Send {
fn run(&self) -> Result<(), String>;
fn name(&self) -> &str {
"job"
}
}
impl<F> Job for F
where
F: Fn() -> Result<(), String> + Send,
{
fn run(&self) -> Result<(), String> {
self()
}
}
struct Envelope {
job: Box<dyn Job>,
attempt: u32,
}
pub struct JobQueue {
sender: Sender<Envelope>,
workers: Vec<thread::JoinHandle<()>>,
max_retries: Arc<AtomicU32>,
initial_backoff_ms: Arc<AtomicU64>,
backoff_multiplier: Arc<AtomicU32>,
}
impl JobQueue {
pub fn new(workers: usize) -> Self {
assert!(workers > 0, "JobQueue requires at least one worker");
let (sender, receiver) = mpsc::channel::<Envelope>();
let receiver = Arc::new(std::sync::Mutex::new(receiver));
let max_retries = Arc::new(AtomicU32::new(3));
let initial_backoff_ms = Arc::new(AtomicU64::new(500));
let backoff_multiplier = Arc::new(AtomicU32::new(2));
let mut handles = Vec::with_capacity(workers);
for _ in 0..workers {
let receiver = Arc::clone(&receiver);
let max_retries = Arc::clone(&max_retries);
let initial_backoff_ms = Arc::clone(&initial_backoff_ms);
let backoff_multiplier = Arc::clone(&backoff_multiplier);
handles.push(thread::spawn(move || {
worker_loop(receiver, max_retries, initial_backoff_ms, backoff_multiplier);
}));
}
JobQueue {
sender,
workers: handles,
max_retries,
initial_backoff_ms,
backoff_multiplier,
}
}
pub fn max_retries(self, n: u32) -> Self {
self.max_retries.store(n, Ordering::Relaxed);
self
}
pub fn backoff(self, initial: Duration, multiplier: u32) -> Self {
self.initial_backoff_ms.store(initial.as_millis() as u64, Ordering::Relaxed);
self.backoff_multiplier.store(multiplier.max(1), Ordering::Relaxed);
self
}
pub fn submit(&self, job: impl Job + 'static) {
let _ = self.sender.send(Envelope { job: Box::new(job), attempt: 0 });
}
pub fn join(self) {
let JobQueue { sender, workers, .. } = self;
drop(sender);
for handle in workers {
let _ = handle.join();
}
}
}
fn worker_loop(
receiver: Arc<std::sync::Mutex<mpsc::Receiver<Envelope>>>,
max_retries: Arc<AtomicU32>,
initial_backoff_ms: Arc<AtomicU64>,
backoff_multiplier: Arc<AtomicU32>,
) {
loop {
let received = {
let lock = receiver.lock().unwrap();
lock.recv()
};
let mut envelope = match received {
Ok(envelope) => envelope,
Err(_) => break, };
loop {
match envelope.job.run() {
Ok(()) => break,
Err(e) => {
let max = max_retries.load(Ordering::Relaxed);
if envelope.attempt >= max {
eprintln!(
"[jobs] '{}' failed permanently after {} attempt(s): {}",
envelope.job.name(),
envelope.attempt + 1,
e
);
break;
}
let initial_ms = initial_backoff_ms.load(Ordering::Relaxed);
let multiplier = backoff_multiplier.load(Ordering::Relaxed).max(1) as u64;
let delay = Duration::from_millis(
initial_ms.saturating_mul(multiplier.saturating_pow(envelope.attempt)),
);
eprintln!(
"[jobs] '{}' failed (attempt {}/{}): {} — retrying in {:?}",
envelope.job.name(),
envelope.attempt + 1,
max + 1,
e,
delay
);
thread::sleep(delay);
envelope.attempt += 1;
}
}
}
}
}
#[cfg(any(feature = "model-sqlite", feature = "model-postgres", feature = "model-mysql"))]
mod persistent;
#[cfg(any(feature = "model-sqlite", feature = "model-postgres", feature = "model-mysql"))]
pub use persistent::{EnqueueOptions, JobInfo, PersistentJobQueue};