Skip to main content

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}