1use std::collections::HashMap;
37use std::future::Future;
38use std::pin::Pin;
39use std::sync::Arc;
40use std::time::Duration;
41
42use async_trait::async_trait;
43use chrono::{DateTime, SecondsFormat, Utc};
44use serde::de::DeserializeOwned;
45use serde::{Deserialize, Serialize};
46
47use crate::cache::Cache;
48use crate::config::QueueConfig;
49use crate::error::{CoreError, CoreResult};
50use crate::events::{Event, Events};
51use crate::parameters::Parameters;
52use crate::registry::Registry;
53use crate::{Db, Module, ModuleId};
54
55mod database;
56pub mod migrations;
57#[cfg(feature = "redis")]
58pub mod redis;
59mod worker;
60
61pub use database::DatabaseQueue;
62pub use worker::{WorkOptions, Worker};
63
64pub struct QueueModule;
67
68impl Module for QueueModule {
69 fn id(&self) -> ModuleId {
70 ModuleId::new(migrations::MODULE_ID)
71 }
72 fn migrations(&self) -> crate::MigrationSet {
73 migrations::migrations()
74 }
75}
76
77pub const DEFAULT_QUEUE: &str = "default";
79
80#[derive(Debug, Clone, PartialEq, Eq)]
82pub enum JobError {
83 Retry(String),
85 Fail(String),
87}
88
89impl JobError {
90 pub fn retry(message: impl std::fmt::Display) -> Self {
92 Self::Retry(message.to_string())
93 }
94
95 pub fn fail(message: impl std::fmt::Display) -> Self {
97 Self::Fail(message.to_string())
98 }
99
100 pub fn message(&self) -> &str {
101 match self {
102 Self::Retry(m) | Self::Fail(m) => m,
103 }
104 }
105}
106
107impl std::fmt::Display for JobError {
108 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
109 f.write_str(self.message())
110 }
111}
112
113impl std::error::Error for JobError {}
114
115impl From<CoreError> for JobError {
116 fn from(e: CoreError) -> Self {
117 Self::Retry(e.to_string())
118 }
119}
120
121impl From<sqlx::Error> for JobError {
122 fn from(e: sqlx::Error) -> Self {
123 Self::Retry(e.to_string())
124 }
125}
126
127#[derive(Clone)]
129pub struct JobCx {
130 db: Db,
131 events: Events,
132 cache: Cache,
133 parameters: Parameters,
134 id: Option<i64>,
135 attempt: u32,
136 queue: String,
137}
138
139impl JobCx {
140 pub fn db(&self) -> &Db {
141 &self.db
142 }
143 pub fn events(&self) -> &Events {
144 &self.events
145 }
146 pub fn cache(&self) -> &Cache {
147 &self.cache
148 }
149 pub fn parameters(&self) -> &Parameters {
150 &self.parameters
151 }
152 pub fn id(&self) -> Option<i64> {
154 self.id
155 }
156 pub fn attempt(&self) -> u32 {
158 self.attempt
159 }
160 pub fn queue(&self) -> &str {
161 &self.queue
162 }
163}
164
165#[async_trait]
168pub trait Job: Serialize + DeserializeOwned + Send + Sync + 'static {
169 const NAME: &'static str;
171
172 async fn run(&self, cx: &JobCx) -> Result<(), JobError>;
173
174 fn queue(&self) -> String {
176 DEFAULT_QUEUE.to_string()
177 }
178
179 fn tries(&self) -> u32 {
182 3
183 }
184
185 fn backoff(&self, attempt: u32) -> Duration {
188 Duration::from_secs(10u64 << attempt.saturating_sub(1).min(8))
189 }
190
191 fn timeout(&self) -> Option<Duration> {
194 Some(Duration::from_secs(60))
195 }
196
197 fn retry_until(&self) -> Option<DateTime<Utc>> {
200 None
201 }
202
203 fn unique_key(&self) -> Option<String> {
206 None
207 }
208}
209
210#[derive(Debug, Clone)]
212pub struct JobMeta {
213 pub tries: u32,
214 pub timeout: Option<Duration>,
215 pub retry_until: Option<DateTime<Utc>>,
216}
217
218type RunFn = dyn Fn(serde_json::Value, JobCx) -> Pin<Box<dyn Future<Output = Result<(), JobError>> + Send>>
219 + Send
220 + Sync;
221type MetaFn = dyn Fn(&serde_json::Value) -> Result<JobMeta, String> + Send + Sync;
222type BackoffFn = dyn Fn(&serde_json::Value, u32) -> Duration + Send + Sync;
223
224#[derive(Clone)]
227pub struct JobReg {
228 name: &'static str,
229 run: Arc<RunFn>,
230 meta: Arc<MetaFn>,
231 backoff: Arc<BackoffFn>,
232}
233
234impl std::fmt::Debug for JobReg {
235 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
236 f.debug_struct("JobReg").field("name", &self.name).finish()
237 }
238}
239
240impl JobReg {
241 pub fn of<T: Job>() -> Self {
243 Self {
244 name: T::NAME,
245 run: Arc::new(|payload, cx| {
246 Box::pin(async move {
247 let job: T = serde_json::from_value(payload)
248 .map_err(|e| JobError::Fail(format!("payload does not decode: {e}")))?;
249 job.run(&cx).await
250 })
251 }),
252 meta: Arc::new(|payload| {
253 let job: T = serde_json::from_value(payload.clone())
254 .map_err(|e| format!("payload does not decode: {e}"))?;
255 Ok(JobMeta {
256 tries: job.tries(),
257 timeout: job.timeout(),
258 retry_until: job.retry_until(),
259 })
260 }),
261 backoff: Arc::new(|payload, attempt| {
262 serde_json::from_value::<T>(payload.clone())
263 .map(|job| job.backoff(attempt))
264 .unwrap_or(Duration::from_secs(10))
265 }),
266 }
267 }
268
269 pub fn name(&self) -> &'static str {
270 self.name
271 }
272}
273
274pub trait QueueRegistry {
276 fn add_job<T: Job>(&mut self);
277}
278
279impl QueueRegistry for Registry {
280 fn add_job<T: Job>(&mut self) {
281 self.add(JobReg::of::<T>());
282 }
283}
284
285#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
287pub struct Queued {
288 pub id: i64,
289 pub queue: String,
290 pub name: String,
291 pub payload: serde_json::Value,
292 pub attempts: u32,
294 pub available_at: String,
295 pub reserved_at: Option<String>,
296 pub reserved_by: Option<String>,
297 pub created_at: String,
298}
299
300#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
302pub struct FailedJob {
303 pub id: i64,
304 pub queue: String,
305 pub name: String,
306 pub payload: serde_json::Value,
307 pub error: String,
308 pub failed_at: String,
309}
310
311#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
313pub struct QueueCount {
314 pub queue: String,
315 pub pending: u64,
317 pub reserved: u64,
319}
320
321#[derive(Debug, Clone, Copy, PartialEq, Eq)]
323pub enum Pushed {
324 Queued(i64),
326 Ran,
328 Duplicate,
330}
331
332#[async_trait]
335pub trait QueueDriver: Send + Sync {
336 fn name(&self) -> &'static str;
337 async fn push(
339 &self,
340 queue: &str,
341 name: &str,
342 payload: &serde_json::Value,
343 available_at: DateTime<Utc>,
344 unique_key: Option<&str>,
345 ) -> CoreResult<Option<i64>>;
346 async fn claim(
349 &self,
350 queues: &[String],
351 retry_after: Duration,
352 worker: &str,
353 ) -> CoreResult<Option<Queued>>;
354 async fn release(&self, id: i64, available_at: DateTime<Utc>) -> CoreResult<()>;
356 async fn delete(&self, id: i64) -> CoreResult<()>;
358 async fn fail(&self, job: &Queued, error: &str) -> CoreResult<i64>;
360 async fn counts(&self) -> CoreResult<Vec<QueueCount>>;
361 async fn failed(&self) -> CoreResult<Vec<FailedJob>>;
362 async fn retry(&self, id: Option<i64>) -> CoreResult<usize>;
365 async fn forget(&self, id: Option<i64>) -> CoreResult<usize>;
367 async fn clear(&self, queue: Option<&str>) -> CoreResult<usize>;
369 async fn ping(&self) -> CoreResult<()> {
371 Ok(())
372 }
373}
374
375#[derive(Debug, Clone, Copy, PartialEq, Eq)]
377pub enum Mode {
378 Worker,
380 External,
382 Inline,
384}
385
386impl Mode {
387 pub fn parse(s: &str) -> CoreResult<Self> {
388 match s {
389 "worker" => Ok(Self::Worker),
390 "external" => Ok(Self::External),
391 "inline" => Ok(Self::Inline),
392 other => Err(CoreError::Config(format!(
393 "queue.mode `{other}` is not one of worker, external, inline"
394 ))),
395 }
396 }
397}
398
399#[derive(Clone)]
401pub struct Services {
402 pub db: Db,
403 pub events: Events,
404 pub cache: Cache,
405 pub parameters: Parameters,
406}
407
408struct Inner {
409 driver: Box<dyn QueueDriver>,
410 jobs: HashMap<&'static str, JobReg>,
411 services: Services,
412 mode: Mode,
413 queues: Vec<String>,
414 retry_after: Duration,
415 poll: Duration,
416 workers: usize,
417 wake: tokio::sync::Notify,
419}
420
421#[derive(Clone)]
423pub struct Queue {
424 inner: Arc<Inner>,
425}
426
427impl std::fmt::Debug for Queue {
428 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
429 f.debug_struct("Queue")
430 .field("driver", &self.inner.driver.name())
431 .field("mode", &self.inner.mode)
432 .field("jobs", &self.inner.jobs.len())
433 .finish_non_exhaustive()
434 }
435}
436
437#[derive(Debug, Clone)]
439pub struct Options {
440 pub mode: Mode,
441 pub queues: Vec<String>,
443 pub retry_after: Duration,
445 pub poll: Duration,
447 pub workers: usize,
449}
450
451impl Options {
452 pub fn from_config(config: &QueueConfig) -> CoreResult<Self> {
454 let mut queues = config.queues.clone();
455 if queues.is_empty() {
456 queues.push(DEFAULT_QUEUE.to_string());
457 }
458 Ok(Self {
459 mode: Mode::parse(&config.mode)?,
460 queues,
461 retry_after: Duration::from_secs(config.retry_after_seconds.max(1)),
462 poll: Duration::from_secs(config.poll_seconds.max(1)),
463 workers: config.workers,
464 })
465 }
466}
467
468impl Default for Options {
469 fn default() -> Self {
470 Self {
471 mode: Mode::Worker,
472 queues: vec![DEFAULT_QUEUE.to_string()],
473 retry_after: Duration::from_secs(90),
474 poll: Duration::from_secs(1),
475 workers: 2,
476 }
477 }
478}
479
480impl Queue {
481 pub fn new(
483 driver: impl QueueDriver + 'static,
484 jobs: impl IntoIterator<Item = JobReg>,
485 services: Services,
486 options: Options,
487 ) -> Self {
488 let jobs = jobs.into_iter().map(|j| (j.name, j)).collect();
489 Self {
490 inner: Arc::new(Inner {
491 driver: Box::new(driver),
492 jobs,
493 services,
494 mode: options.mode,
495 queues: options.queues,
496 retry_after: options.retry_after,
497 poll: options.poll,
498 workers: options.workers,
499 wake: tokio::sync::Notify::new(),
500 }),
501 }
502 }
503
504 pub fn inline(jobs: impl IntoIterator<Item = JobReg>, services: Services) -> Self {
507 Self::new(
508 NoQueue,
509 jobs,
510 services,
511 Options {
512 mode: Mode::Inline,
513 ..Options::default()
514 },
515 )
516 }
517
518 pub fn mode(&self) -> Mode {
519 self.inner.mode
520 }
521
522 pub fn driver_name(&self) -> &'static str {
523 self.inner.driver.name()
524 }
525
526 pub fn job_names(&self) -> Vec<&'static str> {
528 let mut names: Vec<&'static str> = self.inner.jobs.keys().copied().collect();
529 names.sort_unstable();
530 names
531 }
532
533 pub(crate) fn job(&self, name: &str) -> Option<&JobReg> {
534 self.inner.jobs.get(name)
535 }
536
537 pub(crate) fn services(&self) -> &Services {
538 &self.inner.services
539 }
540
541 pub(crate) fn driver(&self) -> &dyn QueueDriver {
542 self.inner.driver.as_ref()
543 }
544
545 pub(crate) fn queues(&self) -> &[String] {
546 &self.inner.queues
547 }
548
549 pub(crate) fn retry_after(&self) -> Duration {
550 self.inner.retry_after
551 }
552
553 pub(crate) fn poll(&self) -> Duration {
554 self.inner.poll
555 }
556
557 pub(crate) fn wake(&self) -> &tokio::sync::Notify {
558 &self.inner.wake
559 }
560
561 fn check<T: Job>(&self) -> CoreResult<()> {
562 if self.inner.jobs.contains_key(T::NAME) {
563 Ok(())
564 } else {
565 Err(CoreError::Validation(format!(
566 "job `{}` is not registered; add `registry.add_job::<{}>()` to a module",
567 T::NAME,
568 std::any::type_name::<T>()
569 )))
570 }
571 }
572
573 pub async fn push<T: Job>(&self, job: &T) -> CoreResult<Pushed> {
575 self.push_at(job, Utc::now()).await
576 }
577
578 pub async fn push_in<T: Job>(&self, job: &T, delay: Duration) -> CoreResult<Pushed> {
580 let delay = chrono::Duration::from_std(delay).unwrap_or(chrono::Duration::MAX);
581 self.push_at(job, Utc::now() + delay).await
582 }
583
584 pub async fn push_at<T: Job>(&self, job: &T, when: DateTime<Utc>) -> CoreResult<Pushed> {
586 self.check::<T>()?;
587 if self.inner.mode == Mode::Inline {
588 let cx = self.context(None, 1, &job.queue());
589 return match job.run(&cx).await {
590 Ok(()) => Ok(Pushed::Ran),
591 Err(e) => Err(CoreError::Internal(format!(
592 "job `{}` failed: {e}",
593 T::NAME
594 ))),
595 };
596 }
597 let payload = serde_json::to_value(job)
598 .map_err(|e| CoreError::Internal(format!("job `{}` cannot serialize: {e}", T::NAME)))?;
599 let unique = job.unique_key();
600 let stored = self
601 .inner
602 .driver
603 .push(&job.queue(), T::NAME, &payload, when, unique.as_deref())
604 .await?;
605 match stored {
606 Some(id) => {
607 self.inner.wake.notify_waiters();
608 tracing::debug!(job = T::NAME, id, "job queued");
609 Ok(Pushed::Queued(id))
610 }
611 None => Ok(Pushed::Duplicate),
612 }
613 }
614
615 pub async fn push_named(
618 &self,
619 name: &str,
620 payload: &serde_json::Value,
621 queue: &str,
622 ) -> CoreResult<Pushed> {
623 let Some(reg) = self.inner.jobs.get(name) else {
624 return Err(CoreError::Validation(format!(
625 "job `{name}` is not registered"
626 )));
627 };
628 if self.inner.mode == Mode::Inline {
629 let cx = self.context(None, 1, queue);
630 return match (reg.run)(payload.clone(), cx).await {
631 Ok(()) => Ok(Pushed::Ran),
632 Err(e) => Err(CoreError::Internal(format!("job `{name}` failed: {e}"))),
633 };
634 }
635 let stored = self
636 .inner
637 .driver
638 .push(queue, name, payload, Utc::now(), None)
639 .await?;
640 match stored {
641 Some(id) => {
642 self.inner.wake.notify_waiters();
643 Ok(Pushed::Queued(id))
644 }
645 None => Ok(Pushed::Duplicate),
646 }
647 }
648
649 pub(crate) fn context(&self, id: Option<i64>, attempt: u32, queue: &str) -> JobCx {
650 let s = &self.inner.services;
651 JobCx {
652 db: s.db.clone(),
653 events: s.events.clone(),
654 cache: s.cache.clone(),
655 parameters: s.parameters.clone(),
656 id,
657 attempt,
658 queue: queue.to_string(),
659 }
660 }
661
662 pub async fn counts(&self) -> CoreResult<Vec<QueueCount>> {
664 self.inner.driver.counts().await
665 }
666
667 pub async fn failed(&self) -> CoreResult<Vec<FailedJob>> {
669 self.inner.driver.failed().await
670 }
671
672 pub async fn retry(&self, id: Option<i64>) -> CoreResult<usize> {
674 let moved = self.inner.driver.retry(id).await?;
675 if moved > 0 {
676 self.inner.wake.notify_waiters();
677 }
678 Ok(moved)
679 }
680
681 pub async fn forget(&self, id: Option<i64>) -> CoreResult<usize> {
683 self.inner.driver.forget(id).await
684 }
685
686 pub async fn clear(&self, queue: Option<&str>) -> CoreResult<usize> {
688 self.inner.driver.clear(queue).await
689 }
690
691 pub async fn work(
694 &self,
695 options: WorkOptions,
696 stop: tokio::sync::watch::Receiver<bool>,
697 ) -> CoreResult<usize> {
698 Worker::new(self.clone(), options).run(stop).await
699 }
700
701 pub fn spawn_workers(
704 &self,
705 stop: tokio::sync::watch::Receiver<bool>,
706 ) -> Vec<tokio::task::JoinHandle<()>> {
707 (0..self.inner.workers)
708 .map(|n| {
709 let queue = self.clone();
710 let stop = stop.clone();
711 tokio::spawn(async move {
712 let options = WorkOptions::default().name(format!("worker-{n}"));
713 if let Err(e) = queue.work(options, stop).await {
714 tracing::error!(worker = n, error = %e, "worker stopped");
715 }
716 })
717 })
718 .collect()
719 }
720}
721
722struct NoQueue;
724
725#[async_trait]
726impl QueueDriver for NoQueue {
727 fn name(&self) -> &'static str {
728 "inline"
729 }
730 async fn push(
731 &self,
732 _queue: &str,
733 _name: &str,
734 _payload: &serde_json::Value,
735 _available_at: DateTime<Utc>,
736 _unique_key: Option<&str>,
737 ) -> CoreResult<Option<i64>> {
738 Ok(None)
739 }
740 async fn claim(
741 &self,
742 _queues: &[String],
743 _retry_after: Duration,
744 _worker: &str,
745 ) -> CoreResult<Option<Queued>> {
746 Ok(None)
747 }
748 async fn release(&self, _id: i64, _available_at: DateTime<Utc>) -> CoreResult<()> {
749 Ok(())
750 }
751 async fn delete(&self, _id: i64) -> CoreResult<()> {
752 Ok(())
753 }
754 async fn fail(&self, _job: &Queued, _error: &str) -> CoreResult<i64> {
755 Ok(0)
756 }
757 async fn counts(&self) -> CoreResult<Vec<QueueCount>> {
758 Ok(Vec::new())
759 }
760 async fn failed(&self) -> CoreResult<Vec<FailedJob>> {
761 Ok(Vec::new())
762 }
763 async fn retry(&self, _id: Option<i64>) -> CoreResult<usize> {
764 Ok(0)
765 }
766 async fn forget(&self, _id: Option<i64>) -> CoreResult<usize> {
767 Ok(0)
768 }
769 async fn clear(&self, _queue: Option<&str>) -> CoreResult<usize> {
770 Ok(0)
771 }
772}
773
774#[derive(Debug, Clone, Serialize, Deserialize)]
776pub struct JobFailed {
777 pub id: i64,
778 pub name: String,
779 pub queue: String,
780 pub attempts: u32,
781 pub error: String,
782}
783
784impl Event for JobFailed {
785 const NAME: &'static str = "queue.job_failed";
786}
787
788pub async fn from_config(
791 config: &QueueConfig,
792 jobs: impl IntoIterator<Item = JobReg>,
793 services: Services,
794) -> CoreResult<Queue> {
795 let options = Options::from_config(config)?;
796 match config.store.as_str() {
797 "auto" | "database" => Ok(Queue::new(
798 DatabaseQueue::new(services.db.clone()),
799 jobs,
800 services,
801 options,
802 )),
803 #[cfg(feature = "redis")]
804 "redis" => {
805 let url = config.url.as_deref().ok_or_else(|| {
806 CoreError::Config("queue.store = \"redis\" needs queue.url".to_string())
807 })?;
808 let driver = redis::connect(url, &config.prefix).await?;
809 driver.ping().await?;
810 Ok(Queue::new(driver, jobs, services, options))
811 }
812 #[cfg(not(feature = "redis"))]
813 "redis" => Err(CoreError::Config(
814 "queue.store = \"redis\" needs the `redis` feature of laterite-core".to_string(),
815 )),
816 other => Err(CoreError::Config(format!(
817 "queue.store `{other}` is not one of auto, database, redis"
818 ))),
819 }
820}
821
822pub fn time_text(at: DateTime<Utc>) -> String {
823 at.to_rfc3339_opts(SecondsFormat::Micros, true)
824}
825
826#[cfg(test)]
827mod tests {
828 use super::*;
829
830 #[test]
831 fn the_default_backoff_doubles_from_ten_seconds() {
832 #[derive(Serialize, Deserialize)]
833 struct Noop;
834 #[async_trait]
835 impl Job for Noop {
836 const NAME: &'static str = "test::noop";
837 async fn run(&self, _cx: &JobCx) -> Result<(), JobError> {
838 Ok(())
839 }
840 }
841 assert_eq!(Noop.backoff(1), Duration::from_secs(10));
842 assert_eq!(Noop.backoff(2), Duration::from_secs(20));
843 assert_eq!(Noop.backoff(4), Duration::from_secs(80));
844 assert_eq!(Noop.backoff(40), Duration::from_secs(2560));
845 assert_eq!(Noop.tries(), 3);
846 }
847
848 #[test]
849 fn a_mode_parses_or_is_refused() {
850 assert_eq!(Mode::parse("inline").unwrap(), Mode::Inline);
851 assert!(Mode::parse("sometimes").is_err());
852 }
853}