1use std::sync::atomic::{AtomicBool, Ordering};
4use std::sync::Arc;
5use std::time::Duration;
6
7use rainier_cache::LockManager;
8use rainier_container::Container;
9use rainier_events::Dispatcher;
10use rainier_support::{Error, Result};
11
12use crate::job::{JobContext, JobRegistry, QueuedJob};
13use crate::queue::Queue;
14
15#[derive(Debug, Clone)]
17pub struct JobProcessing {
18 pub job: QueuedJob,
20}
21
22#[derive(Debug, Clone)]
24pub struct JobProcessed {
25 pub job: QueuedJob,
27 pub duration: Duration,
29}
30
31#[derive(Debug, Clone)]
33pub struct JobReleased {
34 pub job: QueuedJob,
36 pub delay: Duration,
38 pub error: String,
40}
41
42#[derive(Debug, Clone)]
44pub struct JobFailed {
45 pub job: QueuedJob,
47 pub error: String,
49}
50
51#[derive(Debug, Clone, PartialEq, Eq)]
53pub enum Outcome {
54 Idle,
56 Processed,
58 Released,
60 Failed,
62}
63
64#[derive(Debug, Clone)]
66pub struct WorkerOptions {
67 pub queues: Vec<String>,
70 pub sleep: Duration,
72 pub max_jobs: Option<u64>,
74 pub stop_when_empty: bool,
76 pub timeout: Option<Duration>,
78}
79
80impl Default for WorkerOptions {
81 fn default() -> Self {
82 Self {
83 queues: vec!["default".to_string()],
84 sleep: Duration::from_secs(1),
85 max_jobs: None,
86 stop_when_empty: false,
87 timeout: Some(Duration::from_secs(60)),
88 }
89 }
90}
91
92impl WorkerOptions {
93 pub fn queues(mut self, queues: impl IntoIterator<Item = impl Into<String>>) -> Self {
95 self.queues = queues.into_iter().map(Into::into).collect();
96 self
97 }
98
99 pub fn sleep(mut self, sleep: Duration) -> Self {
101 self.sleep = sleep;
102 self
103 }
104
105 pub fn max_jobs(mut self, max: u64) -> Self {
107 self.max_jobs = Some(max);
108 self
109 }
110
111 pub fn stop_when_empty(mut self) -> Self {
113 self.stop_when_empty = true;
114 self
115 }
116
117 pub fn timeout(mut self, timeout: Option<Duration>) -> Self {
119 self.timeout = timeout;
120 self
121 }
122}
123
124#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
126pub struct WorkerStats {
127 pub processed: u64,
129 pub released: u64,
131 pub failed: u64,
133 pub idles: u64,
135}
136
137impl WorkerStats {
138 pub fn total(&self) -> u64 {
140 self.processed + self.failed
141 }
142}
143
144pub struct Worker {
146 queue: Arc<dyn Queue>,
147 registry: Arc<JobRegistry>,
148 container: Arc<Container>,
149 events: Option<Arc<Dispatcher>>,
150 options: WorkerOptions,
151 stopping: AtomicBool,
152 locks: Option<LockManager>,
159}
160
161impl Worker {
162 pub fn with_locks(mut self, locks: LockManager) -> Self {
167 self.locks = Some(locks);
168 self
169 }
170
171 pub fn new(
173 queue: Arc<dyn Queue>,
174 registry: Arc<JobRegistry>,
175 container: Arc<Container>,
176 ) -> Self {
177 Self {
178 queue,
179 registry,
180 container,
181 events: None,
182 locks: None,
183 options: WorkerOptions::default(),
184 stopping: AtomicBool::new(false),
185 }
186 }
187
188 pub fn with_events(mut self, events: Arc<Dispatcher>) -> Self {
190 self.events = Some(events);
191 self
192 }
193
194 pub fn with_options(mut self, options: WorkerOptions) -> Self {
196 self.options = options;
197 self
198 }
199
200 pub fn stop(&self) {
202 self.stopping.store(true, Ordering::SeqCst);
203 }
204
205 pub fn is_stopping(&self) -> bool {
207 self.stopping.load(Ordering::SeqCst)
208 }
209
210 pub async fn run_next(&self) -> Result<Outcome> {
212 for queue in &self.options.queues {
213 if let Some(job) = self.queue.reserve(queue).await? {
214 return self.process(job).await;
215 }
216 }
217 Ok(Outcome::Idle)
218 }
219
220 pub async fn run(&self) -> Result<WorkerStats> {
227 let mut stats = WorkerStats::default();
228
229 loop {
230 if self.is_stopping() {
231 break;
232 }
233 if self.options.max_jobs.is_some_and(|max| stats.total() >= max) {
234 break;
235 }
236
237 match self.run_next().await? {
238 Outcome::Idle => {
239 stats.idles += 1;
240 if self.options.stop_when_empty {
241 break;
242 }
243 tokio::time::sleep(self.options.sleep).await;
244 }
245 Outcome::Processed => stats.processed += 1,
246 Outcome::Released => stats.released += 1,
247 Outcome::Failed => stats.failed += 1,
248 }
249
250 self.container.flush_scoped();
251 }
252
253 Ok(stats)
254 }
255
256 async fn process(&self, job: QueuedJob) -> Result<Outcome> {
257 self.dispatch(JobProcessing { job: job.clone() }).await;
258
259 let context = Arc::new(JobContext::new(
260 Arc::clone(&self.container),
261 job.id.clone(),
262 job.queue.clone(),
263 job.attempts,
264 job.max_attempts,
265 ));
266
267 let started = std::time::Instant::now();
268 let outcome = match self.options.timeout {
269 Some(timeout) => {
270 match tokio::time::timeout(timeout, self.registry.run(&job, Arc::clone(&context)))
271 .await
272 {
273 Ok(result) => result,
274 Err(_) => {
275 Err(Error::internal(format!("the job exceeded its {timeout:?} timeout")))
276 }
277 }
278 }
279 None => self.registry.run(&job, Arc::clone(&context)).await,
280 };
281
282 match outcome {
283 Ok(()) => {
284 self.queue.acknowledge(&job).await?;
285 self.release_uniqueness(&job).await;
286
287 let duration = started.elapsed();
288 tracing::info!(job = %job.name, id = %job.id, ?duration, "job processed");
289 self.dispatch(JobProcessed { job, duration }).await;
290 Ok(Outcome::Processed)
291 }
292 Err(error) => self.handle_failure(job, error).await,
293 }
294 }
295
296 async fn release_uniqueness(&self, job: &QueuedJob) {
306 let (Some(key), Some(locks)) = (&job.unique_key, &self.locks) else { return };
307
308 if let Err(e) = locks.lock(key.clone(), Duration::from_secs(1)).force_release().await {
312 tracing::warn!(
313 job = %job.name,
314 key,
315 error = %e.message(),
316 "could not release the uniqueness lock; it will expire on its TTL"
317 );
318 }
319 }
320
321 async fn handle_failure(&self, job: QueuedJob, error: Error) -> Result<Outcome> {
322 let message = error.to_string();
323
324 if job.is_exhausted() {
325 self.queue.fail(&job, &message).await?;
326
327 self.release_uniqueness(&job).await;
331
332 tracing::error!(job = %job.name, id = %job.id, error = %message, "job failed");
333 self.dispatch(JobFailed { job, error: message }).await;
334 return Ok(Outcome::Failed);
335 }
336
337 let delay = backoff_for(job.attempts);
340 self.queue.release(&job, delay).await?;
341 tracing::warn!(
342 job = %job.name,
343 id = %job.id,
344 attempt = job.attempts,
345 of = job.max_attempts,
346 ?delay,
347 error = %message,
348 "job failed; retrying"
349 );
350 self.dispatch(JobReleased { job, delay, error: message }).await;
351 Ok(Outcome::Released)
352 }
353
354 async fn dispatch<E: Send + Sync + 'static>(&self, event: E) {
357 if let Some(events) = &self.events {
358 events.dispatch_quietly(event).await;
359 }
360 }
361}
362
363fn backoff_for(attempts: u32) -> Duration {
369 Duration::from_secs(1u64 << attempts.clamp(1, 6))
370}
371
372impl std::fmt::Debug for Worker {
373 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
374 f.debug_struct("Worker")
375 .field("queue", &self.queue.name())
376 .field("queues", &self.options.queues)
377 .field("stopping", &self.is_stopping())
378 .finish()
379 }
380}
381
382#[cfg(test)]
383#[allow(clippy::await_holding_lock)]
388mod tests {
389 use super::*;
390 use crate::job::{Job, QueuedJob};
391 use crate::queue::MemoryQueue;
392 use serde::{Deserialize, Serialize};
393 use std::sync::atomic::AtomicU32;
394 use std::sync::Mutex;
395
396 static SERIAL: Mutex<()> = Mutex::new(());
399 static ATTEMPTS: AtomicU32 = AtomicU32::new(0);
400 static SUCCEED_FROM: AtomicU32 = AtomicU32::new(1);
401
402 #[derive(Serialize, Deserialize)]
403 struct Flaky;
404
405 #[async_trait::async_trait]
406 impl Job for Flaky {
407 const NAME: &'static str = "test.flaky";
408 const TRIES: u32 = 3;
409
410 async fn handle(&self, context: &JobContext) -> Result<()> {
411 ATTEMPTS.fetch_add(1, Ordering::SeqCst);
412 if context.attempt() >= SUCCEED_FROM.load(Ordering::SeqCst) {
413 Ok(())
414 } else {
415 Err(Error::internal("not yet"))
416 }
417 }
418 }
419
420 #[derive(Serialize, Deserialize)]
421 struct Slow;
422
423 #[async_trait::async_trait]
424 impl Job for Slow {
425 const NAME: &'static str = "test.slow";
426 const TRIES: u32 = 1;
427
428 async fn handle(&self, _: &JobContext) -> Result<()> {
429 tokio::time::sleep(Duration::from_secs(30)).await;
430 Ok(())
431 }
432 }
433
434 fn registry() -> Arc<JobRegistry> {
435 Arc::new(JobRegistry::new().with::<Flaky>().with::<Slow>())
436 }
437
438 fn worker(queue: Arc<MemoryQueue>) -> Worker {
439 Worker::new(queue, registry(), Arc::new(Container::new()))
440 .with_options(WorkerOptions::default().stop_when_empty())
441 }
442
443 fn reset(succeed_from: u32) -> std::sync::MutexGuard<'static, ()> {
446 let guard = SERIAL.lock().unwrap_or_else(|e| e.into_inner());
447 ATTEMPTS.store(0, Ordering::SeqCst);
448 SUCCEED_FROM.store(succeed_from, Ordering::SeqCst);
449 guard
450 }
451
452 #[tokio::test]
453 async fn an_empty_queue_is_idle() {
454 let queue = Arc::new(MemoryQueue::new());
455 assert_eq!(worker(queue).run_next().await.unwrap(), Outcome::Idle);
456 }
457
458 #[tokio::test]
459 async fn a_successful_job_is_acknowledged() {
460 let _serial = reset(1);
461 let queue = Arc::new(MemoryQueue::new());
462 queue.push(QueuedJob::from_job(&Flaky).unwrap()).await.unwrap();
463
464 assert_eq!(worker(Arc::clone(&queue)).run_next().await.unwrap(), Outcome::Processed);
465 assert_eq!(queue.size("default").await.unwrap(), 0);
466 assert_eq!(queue.reserved_count(), 0);
467 assert!(queue.failed_jobs().is_empty());
468 }
469
470 #[tokio::test]
471 async fn a_failing_job_is_released_for_another_attempt() {
472 let _serial = reset(99); let queue = Arc::new(MemoryQueue::new());
474 queue.push(QueuedJob::from_job(&Flaky).unwrap()).await.unwrap();
475
476 assert_eq!(worker(Arc::clone(&queue)).run_next().await.unwrap(), Outcome::Released);
477 assert_eq!(queue.size("default").await.unwrap(), 1, "it is back in the queue");
478 assert!(queue.failed_jobs().is_empty(), "not failed yet — it has attempts left");
479 }
480
481 #[tokio::test]
482 async fn a_job_that_exhausts_its_attempts_fails() {
483 let _serial = reset(99);
484 let queue = Arc::new(MemoryQueue::new());
485 let mut job = QueuedJob::from_job(&Flaky).unwrap();
486 job.attempts = job.max_attempts - 1; queue.push(job).await.unwrap();
489 assert_eq!(worker(Arc::clone(&queue)).run_next().await.unwrap(), Outcome::Failed);
490
491 let failed = queue.failed_jobs();
492 assert_eq!(failed.len(), 1);
493 assert!(failed[0].error.contains("not yet"), "{}", failed[0].error);
494 assert_eq!(queue.size("default").await.unwrap(), 0);
495 }
496
497 #[tokio::test]
498 async fn a_job_that_succeeds_on_retry_is_only_run_until_it_does() {
499 let _serial = reset(2); let queue = Arc::new(MemoryQueue::new());
501 queue.push(QueuedJob::from_job(&Flaky).unwrap()).await.unwrap();
502
503 let worker = Worker::new(
505 Arc::clone(&queue) as Arc<dyn Queue>,
506 registry(),
507 Arc::new(Container::new()),
508 )
509 .with_options(WorkerOptions::default().stop_when_empty());
510
511 assert_eq!(worker.run_next().await.unwrap(), Outcome::Released);
512 assert_eq!(worker.run_next().await.unwrap(), Outcome::Idle);
514 assert_eq!(ATTEMPTS.load(Ordering::SeqCst), 1);
515 }
516
517 #[tokio::test]
518 async fn a_run_drains_the_queue_and_reports_what_it_did() {
519 let _serial = reset(1);
520 let queue = Arc::new(MemoryQueue::new());
521 for _ in 0..3 {
522 queue.push(QueuedJob::from_job(&Flaky).unwrap()).await.unwrap();
523 }
524
525 let stats = worker(Arc::clone(&queue)).run().await.unwrap();
526 assert_eq!(stats.processed, 3);
527 assert_eq!(stats.failed, 0);
528 assert_eq!(stats.total(), 3);
529 }
530
531 #[tokio::test]
532 async fn a_worker_stops_after_its_job_limit() {
533 let _serial = reset(1);
534 let queue = Arc::new(MemoryQueue::new());
535 for _ in 0..5 {
536 queue.push(QueuedJob::from_job(&Flaky).unwrap()).await.unwrap();
537 }
538
539 let worker = Worker::new(
540 Arc::clone(&queue) as Arc<dyn Queue>,
541 registry(),
542 Arc::new(Container::new()),
543 )
544 .with_options(WorkerOptions::default().stop_when_empty().max_jobs(2));
545
546 assert_eq!(worker.run().await.unwrap().processed, 2);
547 assert_eq!(queue.size("default").await.unwrap(), 3, "the rest are still waiting");
548 }
549
550 #[tokio::test]
551 async fn queues_are_served_in_priority_order() {
552 let _serial = reset(1);
553 let queue = Arc::new(MemoryQueue::new());
554 queue.push(QueuedJob::from_job(&Flaky).unwrap().on_queue("low")).await.unwrap();
555 queue.push(QueuedJob::from_job(&Flaky).unwrap().on_queue("high")).await.unwrap();
556
557 let worker = Worker::new(
558 Arc::clone(&queue) as Arc<dyn Queue>,
559 registry(),
560 Arc::new(Container::new()),
561 )
562 .with_options(WorkerOptions::default().queues(["high", "low"]).stop_when_empty());
563
564 worker.run_next().await.unwrap();
565 assert_eq!(queue.size("high").await.unwrap(), 0, "the high queue drains first");
566 assert_eq!(queue.size("low").await.unwrap(), 1);
567 }
568
569 #[tokio::test]
570 async fn a_job_that_overruns_its_timeout_is_abandoned() {
571 let queue = Arc::new(MemoryQueue::new());
572 queue.push(QueuedJob::from_job(&Slow).unwrap()).await.unwrap();
573
574 let worker = Worker::new(
575 Arc::clone(&queue) as Arc<dyn Queue>,
576 registry(),
577 Arc::new(Container::new()),
578 )
579 .with_options(
580 WorkerOptions::default().stop_when_empty().timeout(Some(Duration::from_millis(50))),
581 );
582
583 assert_eq!(worker.run_next().await.unwrap(), Outcome::Failed);
585 assert!(queue.failed_jobs()[0].error.contains("timeout"));
586 }
587
588 #[tokio::test]
589 async fn an_unknown_job_fails_rather_than_stalling_the_worker() {
590 let queue = Arc::new(MemoryQueue::new());
591 let mut job = QueuedJob::from_job(&Flaky).unwrap();
592 job.name = "never.registered".into();
593 job.max_attempts = 1;
594 queue.push(job).await.unwrap();
595
596 assert_eq!(worker(Arc::clone(&queue)).run_next().await.unwrap(), Outcome::Failed);
597 assert!(queue.failed_jobs()[0].error.contains("never.registered"));
598 }
599
600 #[tokio::test]
601 async fn lifecycle_events_are_fired() {
602 let _serial = reset(1);
603 let queue = Arc::new(MemoryQueue::new());
604 queue.push(QueuedJob::from_job(&Flaky).unwrap()).await.unwrap();
605
606 let events = Arc::new(Dispatcher::new());
607 let log = Arc::new(Mutex::new(Vec::new()));
608
609 let sink = Arc::clone(&log);
610 events.listen(move |_: Arc<JobProcessing>| {
611 let sink = Arc::clone(&sink);
612 async move {
613 sink.lock().unwrap().push("processing");
614 Ok(())
615 }
616 });
617 let sink = Arc::clone(&log);
618 events.listen(move |_: Arc<JobProcessed>| {
619 let sink = Arc::clone(&sink);
620 async move {
621 sink.lock().unwrap().push("processed");
622 Ok(())
623 }
624 });
625
626 worker(Arc::clone(&queue)).with_events(events).run_next().await.unwrap();
627 assert_eq!(*log.lock().unwrap(), vec!["processing", "processed"]);
628 }
629
630 #[tokio::test]
631 async fn a_failing_event_listener_does_not_stop_the_queue() {
632 let _serial = reset(1);
633 let queue = Arc::new(MemoryQueue::new());
634 queue.push(QueuedJob::from_job(&Flaky).unwrap()).await.unwrap();
635
636 let events = Arc::new(Dispatcher::new());
637 events.listen(|_: Arc<JobProcessed>| async { Err(Error::internal("metrics down")) });
638
639 let outcome = worker(Arc::clone(&queue)).with_events(events).run_next().await.unwrap();
640 assert_eq!(outcome, Outcome::Processed, "the job still succeeded");
641 }
642
643 #[tokio::test]
644 async fn a_stopping_worker_leaves_the_loop() {
645 let queue = Arc::new(MemoryQueue::new());
646 let worker = worker(queue);
647 worker.stop();
648
649 assert!(worker.is_stopping());
650 assert_eq!(worker.run().await.unwrap(), WorkerStats::default());
651 }
652
653 #[test]
654 fn the_backoff_curve_grows_and_levels_off() {
655 assert_eq!(backoff_for(1), Duration::from_secs(2));
656 assert_eq!(backoff_for(2), Duration::from_secs(4));
657 assert_eq!(backoff_for(6), Duration::from_secs(64));
658 assert_eq!(backoff_for(50), Duration::from_secs(64), "capped");
659 }
660}