use std::time::Duration;
use chrono::Utc;
use super::{JobError, JobFailed, Queue, Queued};
use crate::error::CoreResult;
#[derive(Debug, Clone)]
pub struct WorkOptions {
pub queues: Vec<String>,
pub once: bool,
pub stop_when_empty: bool,
pub name: String,
}
impl Default for WorkOptions {
fn default() -> Self {
Self {
queues: Vec::new(),
once: false,
stop_when_empty: false,
name: format!("{}-{:x}", hostname(), rand::random::<u32>()),
}
}
}
impl WorkOptions {
pub fn queues(mut self, queues: Vec<String>) -> Self {
self.queues = queues;
self
}
pub fn once(mut self, once: bool) -> Self {
self.once = once;
self
}
pub fn stop_when_empty(mut self, stop: bool) -> Self {
self.stop_when_empty = stop;
self
}
pub fn name(mut self, name: impl Into<String>) -> Self {
self.name = name.into();
self
}
}
fn hostname() -> String {
std::env::var("HOSTNAME")
.ok()
.filter(|h| !h.is_empty())
.unwrap_or_else(|| format!("pid{}", std::process::id()))
}
pub struct Worker {
queue: Queue,
options: WorkOptions,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Outcome {
Done,
Released { attempt: u32, retry_in: Duration },
Failed { attempt: u32, error: String },
}
impl Worker {
pub fn new(queue: Queue, options: WorkOptions) -> Self {
Self { queue, options }
}
pub async fn run(&self, mut stop: tokio::sync::watch::Receiver<bool>) -> CoreResult<usize> {
let queues: Vec<String> = if self.options.queues.is_empty() {
self.queue.queues().to_vec()
} else {
self.options.queues.clone()
};
let mut processed = 0usize;
loop {
if *stop.borrow() {
return Ok(processed);
}
let claimed = self
.queue
.driver()
.claim(&queues, self.queue.retry_after(), &self.options.name)
.await;
match claimed {
Ok(Some(job)) => {
self.process(job).await;
processed += 1;
if self.options.once {
return Ok(processed);
}
}
Ok(None) => {
if self.options.once || self.options.stop_when_empty {
return Ok(processed);
}
tokio::select! {
_ = self.queue.wake().notified() => {}
_ = tokio::time::sleep(self.queue.poll()) => {}
_ = stop.changed() => {}
}
}
Err(e) => {
tracing::error!(error = %e, "claiming a job failed");
tokio::select! {
_ = tokio::time::sleep(self.queue.poll()) => {}
_ = stop.changed() => {}
}
}
}
}
}
pub async fn process(&self, job: Queued) -> Outcome {
let Some(reg) = self.queue.job(&job.name) else {
return self
.fail(&job, &format!("job `{}` is not registered", job.name))
.await;
};
let meta = match (reg.meta)(&job.payload) {
Ok(meta) => meta,
Err(e) => return self.fail(&job, &e).await,
};
let now = Utc::now();
let window_passed = meta.retry_until.is_some_and(|until| now > until);
let tries_spent = meta.retry_until.is_none() && meta.tries > 0 && job.attempts > meta.tries;
if window_passed || tries_spent {
return self
.fail(
&job,
&format!("attempt {} exceeded the job's tries", job.attempts),
)
.await;
}
let cx = self.queue.context(Some(job.id), job.attempts, &job.queue);
let run = (reg.run)(job.payload.clone(), cx);
let result = match meta.timeout {
Some(limit) => match tokio::time::timeout(limit, run).await {
Ok(result) => result,
Err(_) => Err(JobError::Retry(format!(
"timed out after {} seconds",
limit.as_secs()
))),
},
None => run.await,
};
match result {
Ok(()) => {
if let Err(e) = self.queue.driver().delete(job.id).await {
tracing::error!(job = job.id, error = %e, "finished job could not be removed");
}
tracing::debug!(job = job.id, name = %job.name, attempt = job.attempts, "job done");
Outcome::Done
}
Err(JobError::Fail(message)) => self.fail(&job, &message).await,
Err(JobError::Retry(message)) => {
let now = Utc::now();
let window_passed = meta.retry_until.is_some_and(|until| now >= until);
let last_try =
meta.retry_until.is_none() && meta.tries > 0 && job.attempts >= meta.tries;
if window_passed || last_try {
return self.fail(&job, &message).await;
}
let retry_in = (reg.backoff)(&job.payload, job.attempts);
let due =
now + chrono::Duration::from_std(retry_in).unwrap_or(chrono::Duration::MAX);
if let Err(e) = self.queue.driver().release(job.id, due).await {
tracing::error!(job = job.id, error = %e, "job could not be released");
}
tracing::warn!(
job = job.id,
name = %job.name,
attempt = job.attempts,
retry_in = retry_in.as_secs(),
error = %message,
"job failed; will retry"
);
Outcome::Released {
attempt: job.attempts,
retry_in,
}
}
}
}
async fn fail(&self, job: &Queued, error: &str) -> Outcome {
match self.queue.driver().fail(job, error).await {
Ok(failed_id) => {
tracing::error!(
job = job.id,
name = %job.name,
attempt = job.attempts,
error,
"job failed"
);
self.queue
.services()
.events
.emit(&JobFailed {
id: failed_id,
name: job.name.clone(),
queue: job.queue.clone(),
attempts: job.attempts,
error: error.to_string(),
})
.await;
}
Err(e) => {
tracing::error!(job = job.id, error = %e, "failed job could not be recorded");
}
}
Outcome::Failed {
attempt: job.attempts,
error: error.to_string(),
}
}
}