1use super::{Queue, QueueDriver, QueueError, QueuedJob};
4use crate::error_buffer::{ERROR_BUFFER_CAPACITY, ErrorBuffer, ErrorReporter, error_buffer};
5use serde_json::Value;
6use std::collections::HashMap;
7use std::future::Future;
8use std::pin::Pin;
9use std::sync::Arc;
10use std::time::Duration;
11use tokio::sync::watch;
12use tokio::task::{JoinHandle, JoinSet};
13
14mod execution;
15
16pub(crate) const UNHANDLED_JOB_RETRY_DELAY: Duration = Duration::from_secs(5);
18
19pub type JobHandler = Box<
21 dyn Fn(
22 Value,
23 ) -> Pin<
24 Box<dyn Future<Output = Result<(), Box<dyn std::error::Error + Send + Sync>>> + Send>,
25 > + Send
26 + Sync,
27>;
28
29pub struct Worker {
31 driver: Arc<Box<dyn QueueDriver>>,
32 handlers: HashMap<String, Arc<JobHandler>>,
33 poll_interval: Duration,
34 max_concurrency: usize,
35 job_timeout: Duration,
36 stalled_after: Duration,
37}
38
39impl Worker {
40 pub fn new(queue: &Queue) -> Self {
42 Self {
43 driver: queue.driver_ref(),
44 handlers: HashMap::new(),
45 poll_interval: Duration::from_secs(1),
46 max_concurrency: 16,
47 job_timeout: Duration::from_secs(300),
48 stalled_after: Duration::from_secs(600),
49 }
50 }
51
52 pub fn poll_interval(mut self, milliseconds: u64) -> Self {
54 self.poll_interval = Duration::from_millis(milliseconds);
55 self
56 }
57
58 pub fn max_concurrency(mut self, limit: usize) -> Self {
60 self.max_concurrency = limit;
61 self
62 }
63
64 pub fn job_timeout(mut self, timeout: Duration) -> Self {
71 self.job_timeout = timeout;
72 self
73 }
74
75 pub fn stalled_after(mut self, age: Duration) -> Self {
86 self.stalled_after = age;
87 self
88 }
89
90 pub fn register<F, Fut>(&mut self, name: impl Into<String>, handler: F) -> &mut Self
92 where
93 F: Fn(Value) -> Fut + Send + Sync + 'static,
94 Fut: Future<Output = Result<(), Box<dyn std::error::Error + Send + Sync>>> + Send + 'static,
95 {
96 let boxed: JobHandler = Box::new(move |payload| Box::pin(handler(payload)));
97 self.handlers.insert(name.into(), Arc::new(boxed));
98 self
99 }
100
101 #[cfg_attr(mutants, mutants::skip)]
107 pub fn run(&self) -> Result<WorkerHandle, QueueError> {
108 if self.max_concurrency == 0 {
109 return Err(QueueError::InvalidConfiguration(
110 "max_concurrency must be greater than zero".to_string(),
111 ));
112 }
113 if self.poll_interval.is_zero() {
114 return Err(QueueError::InvalidConfiguration(
115 "poll_interval must be greater than zero".to_string(),
116 ));
117 }
118 if self.stalled_after <= self.job_timeout {
119 return Err(QueueError::InvalidConfiguration(
120 "stalled_after must be greater than job_timeout to protect active jobs".to_string(),
121 ));
122 }
123 let runtime =
124 tokio::runtime::Handle::try_current().map_err(|_| QueueError::RuntimeUnavailable)?;
125 let (shutdown, shutdown_rx) = watch::channel(false);
126 let (errors_tx, errors) = error_buffer(ERROR_BUFFER_CAPACITY, "queue_worker");
127 let task = runtime.spawn(run_worker_loop(
128 Arc::clone(&self.driver),
129 self.handlers.clone(),
130 self.poll_interval,
131 self.max_concurrency,
132 self.job_timeout,
133 self.stalled_after,
134 shutdown_rx,
135 errors_tx,
136 ));
137
138 Ok(WorkerHandle {
139 shutdown,
140 task: Some(task),
141 errors,
142 })
143 }
144}
145
146#[must_use = "dropping the worker handle immediately stops queue processing"]
153pub struct WorkerHandle {
154 shutdown: watch::Sender<bool>,
155 task: Option<JoinHandle<()>>,
156 errors: ErrorBuffer<QueueError>,
157}
158
159impl WorkerHandle {
160 pub async fn next_error(&mut self) -> Option<QueueError> {
162 self.errors.next().await
163 }
164
165 pub fn try_next_error(&mut self) -> Option<QueueError> {
167 self.errors.try_next()
168 }
169
170 pub fn dropped_errors(&self) -> u64 {
172 self.errors.dropped()
173 }
174
175 pub async fn shutdown(mut self) -> Result<(), QueueError> {
191 let _ = self.shutdown.send(true);
192 let mut first_error = self.try_next_error();
193 if let Some(task) = self.task.take()
194 && let Err(error) = task.await
195 && !error.is_cancelled()
196 && first_error.is_none()
197 {
198 first_error = Some(QueueError::WorkerTask(error.to_string()));
199 }
200 first_error = first_error.or_else(|| self.try_next_error());
201 match first_error {
202 Some(error) => Err(error),
203 None => Ok(()),
204 }
205 }
206
207 fn abort(&mut self) {
208 let _ = self.shutdown.send(true);
209 if let Some(task) = self.task.take() {
210 task.abort();
211 }
212 }
213}
214
215impl Drop for WorkerHandle {
216 fn drop(&mut self) {
217 self.abort();
218 }
219}
220
221#[allow(clippy::too_many_arguments)]
222#[cfg_attr(mutants, mutants::skip)]
223async fn run_worker_loop(
224 driver: Arc<Box<dyn QueueDriver>>,
225 handlers: HashMap<String, Arc<JobHandler>>,
226 poll_interval: Duration,
227 max_concurrency: usize,
228 job_timeout: Duration,
229 stalled_after: Duration,
230 mut shutdown: watch::Receiver<bool>,
231 errors: ErrorReporter<QueueError>,
232) {
233 match driver.recover_stalled(stalled_after).await {
234 Ok(_) | Err(QueueError::Unsupported(_)) => {}
235 Err(error) => {
236 errors.report(error);
237 }
238 }
239
240 let recovery_task = tokio::spawn(recovery_loop(
241 Arc::clone(&driver),
242 stalled_after,
243 shutdown.clone(),
244 errors.clone(),
245 ));
246 let mut recovery_task = AbortOnDrop(recovery_task);
247
248 let mut jobs = JoinSet::new();
249 loop {
250 if shutdown_requested(&shutdown) {
251 break;
252 }
253 if jobs.len() >= max_concurrency {
254 tokio::select! {
255 outcome = jobs.join_next() => report_outcome(outcome, &errors),
256 _ = wait_for_shutdown(&mut shutdown) => break,
257 }
258 continue;
259 }
260
261 let popped = claim_next(&**driver, &mut jobs, &errors).await;
262 if shutdown_requested(&shutdown) {
263 release_claim_after_shutdown(popped, &**driver, &errors).await;
264 break;
265 }
266 match popped {
267 Ok(Some(job)) => {
268 dispatch_job(
269 job,
270 &handlers,
271 Arc::clone(&driver),
272 job_timeout,
273 shutdown.clone(),
274 &mut jobs,
275 &errors,
276 )
277 .await
278 }
279 Ok(None) => {
280 tokio::select! {
281 _ = tokio::time::sleep(poll_interval) => {}
282 outcome = jobs.join_next(), if !jobs.is_empty() => {
283 report_outcome(outcome, &errors);
284 }
285 _ = wait_for_shutdown(&mut shutdown) => break,
286 }
287 }
288 Err(error) => {
289 errors.report(error);
290 tokio::select! {
291 _ = tokio::time::sleep(poll_interval) => {}
292 _ = wait_for_shutdown(&mut shutdown) => break,
293 }
294 }
295 }
296 }
297
298 while let Some(outcome) = jobs.join_next().await {
299 report_outcome(Some(outcome), &errors);
300 }
301 if let Err(error) = (&mut recovery_task.0).await
302 && !error.is_cancelled()
303 {
304 errors.report(QueueError::WorkerTask(error.to_string()));
305 }
306}
307
308async fn claim_next(
316 driver: &dyn QueueDriver,
317 jobs: &mut JoinSet<Result<(), QueueError>>,
318 errors: &ErrorReporter<QueueError>,
319) -> Result<Option<QueuedJob>, QueueError> {
320 let mut claim = driver.pop();
321 loop {
322 tokio::select! {
323 popped = &mut claim => return popped,
324 outcome = jobs.join_next(), if !jobs.is_empty() => report_outcome(outcome, errors),
325 }
326 }
327}
328
329async fn release_claim_after_shutdown(
332 popped: Result<Option<QueuedJob>, QueueError>,
333 driver: &dyn QueueDriver,
334 errors: &ErrorReporter<QueueError>,
335) {
336 match popped {
337 Ok(Some(job)) => {
338 if let Err(error) = driver
339 .requeue_attempt(&job.id, job.attempts, "worker shutdown before dispatch")
340 .await
341 {
342 errors.report(state_error(&job.id, "requeue_claim_after_shutdown", error));
343 }
344 }
345 Ok(None) => {}
346 Err(error) => {
347 errors.report(error);
348 }
349 }
350}
351
352async fn recovery_loop(
353 driver: Arc<Box<dyn QueueDriver>>,
354 stale_after: Duration,
355 mut shutdown: watch::Receiver<bool>,
356 errors: ErrorReporter<QueueError>,
357) {
358 let interval = stale_after
359 .min(Duration::from_secs(60))
360 .max(Duration::from_secs(1));
361 loop {
362 tokio::select! {
363 _ = tokio::time::sleep(interval) => {}
364 _ = wait_for_shutdown(&mut shutdown) => break,
365 }
366 match driver.recover_stalled(stale_after).await {
367 Ok(_) => {}
368 Err(QueueError::Unsupported(_)) => break,
369 Err(error) => {
370 errors.report(error);
371 }
372 }
373 }
374}
375
376async fn dispatch_job(
377 job: QueuedJob,
378 handlers: &HashMap<String, Arc<JobHandler>>,
379 driver: Arc<Box<dyn QueueDriver>>,
380 timeout: Duration,
381 shutdown: watch::Receiver<bool>,
382 jobs: &mut JoinSet<Result<(), QueueError>>,
383 errors: &ErrorReporter<QueueError>,
384) {
385 let Some(handler) = handlers.get(&job.name).cloned() else {
386 hand_back_unhandled(&job, &**driver, errors).await;
387 return;
388 };
389
390 jobs.spawn(execution::execute_job(
391 driver, handler, job, timeout, shutdown,
392 ));
393}
394
395async fn hand_back_unhandled(
400 job: &QueuedJob,
401 driver: &dyn QueueDriver,
402 errors: &ErrorReporter<QueueError>,
403) {
404 let missing = QueueError::HandlerNotFound(job.name.clone());
405 let reason = missing.to_string();
406 let transition = match driver
407 .requeue_attempt_after(&job.id, job.attempts, &reason, UNHANDLED_JOB_RETRY_DELAY)
408 .await
409 {
410 Err(QueueError::Unsupported(_)) => driver
411 .mark_failed_attempt(&job.id, job.attempts, &reason)
412 .await
413 .map_err(|error| state_error(&job.id, "mark_failed", error)),
414 deferred => deferred.map_err(|error| state_error(&job.id, "requeue_unhandled", error)),
415 };
416 match transition {
417 Ok(()) => errors.report(missing),
418 Err(error) => errors.report(error),
419 }
420}
421
422fn report_outcome(
423 outcome: Option<Result<Result<(), QueueError>, tokio::task::JoinError>>,
424 errors: &ErrorReporter<QueueError>,
425) {
426 match outcome {
427 Some(Ok(Err(error))) => {
428 errors.report(error);
429 }
430 Some(Err(error)) => {
431 errors.report(QueueError::WorkerTask(error.to_string()));
432 }
433 Some(Ok(Ok(()))) | None => {}
434 }
435}
436
437fn state_error(job_id: &str, operation: &'static str, error: QueueError) -> QueueError {
438 QueueError::StateTransition {
439 job_id: job_id.to_string(),
440 operation,
441 message: error.to_string(),
442 }
443}
444
445fn shutdown_requested(shutdown: &watch::Receiver<bool>) -> bool {
446 *shutdown.borrow()
447}
448
449async fn wait_for_shutdown(shutdown: &mut watch::Receiver<bool>) {
450 while !shutdown_requested(shutdown) {
451 if shutdown.changed().await.is_err() {
452 break;
453 }
454 }
455}
456
457fn duration_millis_u64(duration: Duration) -> u64 {
458 u64::try_from(duration.as_millis()).unwrap_or(u64::MAX)
459}
460
461struct AbortOnDrop<T>(JoinHandle<T>);
462
463impl<T> Drop for AbortOnDrop<T> {
464 fn drop(&mut self) {
465 self.0.abort();
466 }
467}