rustlavel_queue/queue.rs
1//! The [`Queue`] contract every driver implements.
2//!
3//! Dyn-compatible on purpose: an application picks its driver at boot and the
4//! rest of the framework holds an `Arc<dyn Queue>` without knowing which one it
5//! got. That is why every method returns a [`BoxFuture`] rather than being an
6//! `async fn` — `async fn` in a trait is not dyn-compatible — and why the trait
7//! speaks in [`QueuedJob`] rather than in a generic `J: Job`.
8//!
9//! The generic conveniences that cannot be dyn-compatible (`dispatch`, which
10//! takes a concrete job type) live in [`QueueExt`], blanket-implemented for
11//! every `Queue` including `dyn Queue`, so callers never notice the split.
12
13use crate::job::{BoxFuture, FailedJob, Job, QueuedJob, ReservedJob};
14use rustlavel_core::events::{self, Event};
15use rustlavel_core::{Json, Result};
16use std::time::Duration;
17
18/// A queue backend.
19pub trait Queue: Send + Sync + 'static {
20 /// The driver's name. It appears in `queue.*` events and error messages,
21 /// which is how Telescope shows which backend handled a job.
22 fn driver(&self) -> &'static str;
23
24 /// Store a job. Returns the driver's identifier for it.
25 ///
26 /// The envelope carries its own queue and delay, so this one primitive is
27 /// all a driver has to implement.
28 fn push(&self, job: QueuedJob) -> BoxFuture<'_, Result<String>>;
29
30 /// Store a job on a named queue, whatever the job itself asked for.
31 fn push_on<'a>(&'a self, queue: &'a str, job: QueuedJob) -> BoxFuture<'a, Result<String>> {
32 self.push(job.on_queue(queue))
33 }
34
35 /// Store a job that only becomes visible to a worker after `delay`.
36 fn later<'a>(&'a self, delay: Duration, job: QueuedJob) -> BoxFuture<'a, Result<String>> {
37 self.push(job.with_delay(delay))
38 }
39
40 /// Reserve the next available job, or `None` when there is nothing ready.
41 ///
42 /// A delayed job whose time has not come is not "available"; neither is one
43 /// another worker already holds. Reserving must be atomic across workers —
44 /// see the database driver for how that is done and why.
45 fn pop<'a>(&'a self, queue: &'a str) -> BoxFuture<'a, Result<Option<ReservedJob>>>;
46
47 /// How many jobs are waiting on a queue, including delayed ones.
48 fn size<'a>(&'a self, queue: &'a str) -> BoxFuture<'a, Result<u64>>;
49
50 /// Finish with a reserved job: it succeeded and is gone.
51 fn delete<'a>(&'a self, job: &'a ReservedJob) -> BoxFuture<'a, Result<()>>;
52
53 /// Put a reserved job back, available again after `delay`.
54 ///
55 /// The attempt count is kept, which is what makes a retry a retry rather
56 /// than a fresh job that will fail forever.
57 fn release<'a>(&'a self, job: &'a ReservedJob, delay: Duration) -> BoxFuture<'a, Result<()>>;
58
59 /// Move a reserved job to the dead-letter store with the error that killed
60 /// it. It must no longer be on the queue afterwards.
61 fn fail<'a>(&'a self, job: &'a ReservedJob, error: &'a str) -> BoxFuture<'a, Result<()>>;
62
63 /// Everything in the dead-letter store, oldest first.
64 fn failed_jobs(&self) -> BoxFuture<'_, Result<Vec<FailedJob>>>;
65
66 /// Throw away every job waiting on a queue. Returns how many went.
67 fn clear<'a>(&'a self, queue: &'a str) -> BoxFuture<'a, Result<u64>>;
68}
69
70/// The conveniences that take a concrete job type, and so cannot live on a
71/// dyn-compatible trait. Blanket-implemented, including for `dyn Queue`.
72pub trait QueueExt: Queue {
73 /// `dispatch(SendWelcomeEmail { user_id })` — the everyday call.
74 /// Push the first step of a chain; the worker pushes each next step as
75 /// the one before it succeeds. Returns the first step's id.
76 fn dispatch_chain<'a>(&'a self, chain: crate::chain::Chain) -> BoxFuture<'a, Result<String>> {
77 Box::pin(async move {
78 if chain.is_empty() {
79 return Err(rustlavel_core::Error::msg("a chain needs at least one step"));
80 }
81 self.push(chain.into_queued()).await
82 })
83 }
84
85 fn dispatch<'a, J: Job>(&'a self, job: &'a J) -> BoxFuture<'a, Result<String>> {
86 self.push(job.to_queued())
87 }
88
89 /// Dispatch onto a named queue instead of the job's own.
90 fn dispatch_on<'a, J: Job>(
91 &'a self,
92 queue: &'a str,
93 job: &'a J,
94 ) -> BoxFuture<'a, Result<String>> {
95 self.push_on(queue, job.to_queued())
96 }
97
98 /// Dispatch, but not before `delay` has passed.
99 fn dispatch_later<'a, J: Job>(
100 &'a self,
101 delay: Duration,
102 job: &'a J,
103 ) -> BoxFuture<'a, Result<String>> {
104 self.later(delay, job.to_queued())
105 }
106}
107
108impl<T: Queue + ?Sized> QueueExt for T {}
109
110/// Report a push on the instrumentation bus.
111///
112/// Guarded by `has_subscribers` so an application with no Telescope never pays
113/// for building an event nobody reads. Drivers call this from `push`; the
114/// worker reports `queue.processed` and `queue.failed` itself.
115pub(crate) fn record_pushed(driver: &'static str, job: &QueuedJob, id: &str) {
116 if !events::has_subscribers() {
117 return;
118 }
119
120 Event::new("queue.pushed")
121 .with("job", job.name.as_str())
122 .with("queue", job.queue.as_str())
123 .with("id", id)
124 .with("driver", driver)
125 .with("delay_seconds", job.delay.as_secs())
126 .dispatch();
127}
128
129/// The JSON body a dashboard or `queue:status` shows.
130pub async fn stats(queue: &dyn Queue, names: &[String]) -> Result<Json> {
131 let mut sizes = Vec::new();
132 for name in names {
133 sizes.push((name.clone(), Json::from(queue.size(name).await?)));
134 }
135
136 let failed = queue.failed_jobs().await?;
137
138 Ok(Json::object([
139 ("driver", Json::from(queue.driver())),
140 ("queues", Json::Object(sizes.into_iter().collect())),
141 ("failed", Json::Array(failed.iter().map(FailedJob::to_json).collect())),
142 ]))
143}