1use std::time::Duration;
4
5use chrono::Utc;
6
7use super::{JobError, JobFailed, Queue, Queued};
8use crate::error::CoreResult;
9
10#[derive(Debug, Clone)]
12pub struct WorkOptions {
13 pub queues: Vec<String>,
16 pub once: bool,
18 pub stop_when_empty: bool,
20 pub name: String,
22}
23
24impl Default for WorkOptions {
25 fn default() -> Self {
26 Self {
27 queues: Vec::new(),
28 once: false,
29 stop_when_empty: false,
30 name: format!("{}-{:x}", hostname(), rand::random::<u32>()),
31 }
32 }
33}
34
35impl WorkOptions {
36 pub fn queues(mut self, queues: Vec<String>) -> Self {
37 self.queues = queues;
38 self
39 }
40 pub fn once(mut self, once: bool) -> Self {
41 self.once = once;
42 self
43 }
44 pub fn stop_when_empty(mut self, stop: bool) -> Self {
45 self.stop_when_empty = stop;
46 self
47 }
48 pub fn name(mut self, name: impl Into<String>) -> Self {
49 self.name = name.into();
50 self
51 }
52}
53
54fn hostname() -> String {
55 std::env::var("HOSTNAME")
56 .ok()
57 .filter(|h| !h.is_empty())
58 .unwrap_or_else(|| format!("pid{}", std::process::id()))
59}
60
61pub struct Worker {
63 queue: Queue,
64 options: WorkOptions,
65}
66
67#[derive(Debug, Clone, PartialEq, Eq)]
69pub enum Outcome {
70 Done,
71 Released { attempt: u32, retry_in: Duration },
72 Failed { attempt: u32, error: String },
73}
74
75impl Worker {
76 pub fn new(queue: Queue, options: WorkOptions) -> Self {
77 Self { queue, options }
78 }
79
80 pub async fn run(&self, mut stop: tokio::sync::watch::Receiver<bool>) -> CoreResult<usize> {
83 let queues: Vec<String> = if self.options.queues.is_empty() {
84 self.queue.queues().to_vec()
85 } else {
86 self.options.queues.clone()
87 };
88 let mut processed = 0usize;
89 loop {
90 if *stop.borrow() {
91 return Ok(processed);
92 }
93 let claimed = self
94 .queue
95 .driver()
96 .claim(&queues, self.queue.retry_after(), &self.options.name)
97 .await;
98 match claimed {
99 Ok(Some(job)) => {
100 self.process(job).await;
101 processed += 1;
102 if self.options.once {
103 return Ok(processed);
104 }
105 }
106 Ok(None) => {
107 if self.options.once || self.options.stop_when_empty {
108 return Ok(processed);
109 }
110 tokio::select! {
111 _ = self.queue.wake().notified() => {}
112 _ = tokio::time::sleep(self.queue.poll()) => {}
113 _ = stop.changed() => {}
114 }
115 }
116 Err(e) => {
117 tracing::error!(error = %e, "claiming a job failed");
118 tokio::select! {
119 _ = tokio::time::sleep(self.queue.poll()) => {}
120 _ = stop.changed() => {}
121 }
122 }
123 }
124 }
125 }
126
127 pub async fn process(&self, job: Queued) -> Outcome {
129 let Some(reg) = self.queue.job(&job.name) else {
130 return self
131 .fail(&job, &format!("job `{}` is not registered", job.name))
132 .await;
133 };
134 let meta = match (reg.meta)(&job.payload) {
135 Ok(meta) => meta,
136 Err(e) => return self.fail(&job, &e).await,
137 };
138 let now = Utc::now();
139 let window_passed = meta.retry_until.is_some_and(|until| now > until);
141 let tries_spent = meta.retry_until.is_none() && meta.tries > 0 && job.attempts > meta.tries;
142 if window_passed || tries_spent {
143 return self
144 .fail(
145 &job,
146 &format!("attempt {} exceeded the job's tries", job.attempts),
147 )
148 .await;
149 }
150 let cx = self.queue.context(Some(job.id), job.attempts, &job.queue);
151 let run = (reg.run)(job.payload.clone(), cx);
152 let result = match meta.timeout {
153 Some(limit) => match tokio::time::timeout(limit, run).await {
154 Ok(result) => result,
155 Err(_) => Err(JobError::Retry(format!(
156 "timed out after {} seconds",
157 limit.as_secs()
158 ))),
159 },
160 None => run.await,
161 };
162 match result {
163 Ok(()) => {
164 if let Err(e) = self.queue.driver().delete(job.id).await {
165 tracing::error!(job = job.id, error = %e, "finished job could not be removed");
166 }
167 tracing::debug!(job = job.id, name = %job.name, attempt = job.attempts, "job done");
168 Outcome::Done
169 }
170 Err(JobError::Fail(message)) => self.fail(&job, &message).await,
171 Err(JobError::Retry(message)) => {
172 let now = Utc::now();
173 let window_passed = meta.retry_until.is_some_and(|until| now >= until);
174 let last_try =
175 meta.retry_until.is_none() && meta.tries > 0 && job.attempts >= meta.tries;
176 if window_passed || last_try {
177 return self.fail(&job, &message).await;
178 }
179 let retry_in = (reg.backoff)(&job.payload, job.attempts);
180 let due =
181 now + chrono::Duration::from_std(retry_in).unwrap_or(chrono::Duration::MAX);
182 if let Err(e) = self.queue.driver().release(job.id, due).await {
183 tracing::error!(job = job.id, error = %e, "job could not be released");
184 }
185 tracing::warn!(
186 job = job.id,
187 name = %job.name,
188 attempt = job.attempts,
189 retry_in = retry_in.as_secs(),
190 error = %message,
191 "job failed; will retry"
192 );
193 Outcome::Released {
194 attempt: job.attempts,
195 retry_in,
196 }
197 }
198 }
199 }
200
201 async fn fail(&self, job: &Queued, error: &str) -> Outcome {
202 match self.queue.driver().fail(job, error).await {
203 Ok(failed_id) => {
204 tracing::error!(
205 job = job.id,
206 name = %job.name,
207 attempt = job.attempts,
208 error,
209 "job failed"
210 );
211 self.queue
212 .services()
213 .events
214 .emit(&JobFailed {
215 id: failed_id,
216 name: job.name.clone(),
217 queue: job.queue.clone(),
218 attempts: job.attempts,
219 error: error.to_string(),
220 })
221 .await;
222 }
223 Err(e) => {
224 tracing::error!(job = job.id, error = %e, "failed job could not be recorded");
225 }
226 }
227 Outcome::Failed {
228 attempt: job.attempts,
229 error: error.to_string(),
230 }
231 }
232}