Skip to main content

rainier_queue/
driver.rs

1//! Where queued jobs wait — [`QueueDriver`].
2
3use rainier_support::setting_enum;
4
5setting_enum! {
6    /// Which [`Queue`](crate::Queue) to build.
7    ///
8    /// ```
9    /// use rainier_queue::QueueDriver;
10    /// use rainier_support::Setting;
11    ///
12    /// assert!(!QueueDriver::Sync.is_deferred(), "sync runs the job inline");
13    /// assert!(QueueDriver::parse("database").unwrap().survives_a_restart());
14    /// ```
15    pub enum QueueDriver: "queue driver" {
16        /// Run the job inline, on the thread that dispatched it.
17        ///
18        /// The default because it needs no worker: a fresh clone dispatches a
19        /// job and the job happens. Also the reason a slow job makes a slow
20        /// request — which is the thing a queue exists to avoid, so this is a
21        /// development setting.
22        #[default]
23        Sync = "sync",
24
25        /// A queue in this process's memory.
26        ///
27        /// For tests that want a job to be *queued* rather than *run*, so they
28        /// can assert on it. Lost on restart, invisible to other processes.
29        Memory = "memory",
30
31        /// Two tables in the application's own database.
32        ///
33        /// Durable, shared, and needs no new infrastructure — the reason it is
34        /// the usual production answer. Needs the migrations
35        /// (`DatabaseQueue::migrations()`) and a `queue:work` process.
36        Database = "database",
37
38        /// Redis streams.
39        ///
40        /// Durable-ish, and the qualifier is the point: Redis acknowledges a
41        /// write before it is on disk and before a replica has it, so a job
42        /// can be lost to a crash or a failover. Right for work you can afford
43        /// to lose, wrong for work you cannot — see the
44        /// [driver's docs](crate::redis). Needs the `redis` feature.
45        Redis = "redis",
46
47        /// Amazon SQS.
48        ///
49        /// Durable and managed, with the visibility timeout doing the
50        /// reservation. Needs the `sqs` feature.
51        Sqs = "sqs",
52
53        /// A Kafka topic.
54        ///
55        /// **Read [the driver's docs](crate::kafka) before choosing it.** A log
56        /// is not a queue: concurrency is the partition count rather than the
57        /// worker count, a delayed job blocks its partition, and a retry goes
58        /// to the end of the topic instead of back where it was. Right when the
59        /// jobs are already events on a topic; wrong as a general job queue.
60        /// Needs the `kafka` feature.
61        Kafka = "kafka",
62    }
63}
64
65impl QueueDriver {
66    /// Whether dispatching returns before the job has run.
67    ///
68    /// `false` for [`Sync`](Self::Sync), which is the whole difference between
69    /// it and the others — and the reason a test that asserts on a side effect
70    /// passes under `sync` and hangs under everything else.
71    pub fn is_deferred(&self) -> bool {
72        !matches!(self, Self::Sync)
73    }
74
75    /// Whether a job dispatched now survives **this** process exiting.
76    ///
77    /// True for anything that stores the job somewhere else. It is not the
78    /// same question as whether the job is safe — see
79    /// [`may_lose_an_accepted_job`](Self::may_lose_an_accepted_job), which is
80    /// the one that catches Redis.
81    pub fn survives_a_restart(&self) -> bool {
82        matches!(self, Self::Database | Self::Redis | Self::Sqs | Self::Kafka)
83    }
84
85    /// Whether a dispatch this driver **accepted** can still be lost.
86    ///
87    /// True only for [`Redis`](Self::Redis), and it is the distinction the
88    /// obvious question misses: a Redis job survives your process restarting,
89    /// because it lives in another one — and can still vanish, because Redis
90    /// acknowledges a write before it is on disk (`appendfsync everysec`
91    /// leaves a window of about a second) and before any replica has it (
92    /// replication is asynchronous, so a failover can drop confirmed writes).
93    ///
94    /// A database queue's insert is committed before it returns; SQS is
95    /// replicated across availability zones before it acknowledges. Neither
96    /// can tell you a job is queued and then lose it.
97    ///
98    /// Kafka is produced to with `acks=all`, so a push returns once every
99    /// in-sync replica holds the record — which is the same guarantee, *and is
100    /// only as good as the replication*. A single-broker cluster has one
101    /// in-sync replica and Kafka does not fsync each write, so a development
102    /// cluster can lose an accepted job even though this reports it cannot.
103    /// `replication.factor` and `min.insync.replicas` of at least two are what
104    /// make the answer true.
105    ///
106    /// So this is the predicate a deploy check should look at when the work is
107    /// a payment rather than a cache warm.
108    pub fn may_lose_an_accepted_job(&self) -> bool {
109        matches!(self, Self::Redis)
110    }
111
112    /// Whether this driver needs a `queue:work` process to make progress.
113    pub fn needs_a_worker(&self) -> bool {
114        self.is_deferred()
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121    use rainier_support::Setting;
122
123    #[test]
124    fn sync_is_the_only_driver_that_runs_inline() {
125        assert!(!QueueDriver::Sync.is_deferred());
126        assert!(!QueueDriver::Sync.needs_a_worker());
127
128        for driver in QueueDriver::ALL.iter().filter(|d| **d != QueueDriver::Sync) {
129            assert!(driver.is_deferred(), "{driver} should defer");
130            assert!(driver.needs_a_worker(), "{driver} should need a worker");
131        }
132    }
133
134    #[test]
135    fn only_the_backed_drivers_survive_a_restart() {
136        assert!(QueueDriver::Database.survives_a_restart());
137        assert!(QueueDriver::Sqs.survives_a_restart());
138        assert!(QueueDriver::Kafka.survives_a_restart());
139        assert!(QueueDriver::Redis.survives_a_restart(), "it lives in another process");
140        assert!(!QueueDriver::Memory.survives_a_restart());
141    }
142
143    #[test]
144    fn only_redis_can_lose_a_job_it_already_accepted() {
145        // The distinction `survives_a_restart` cannot make: the job outlives
146        // your process *and* can still be gone, because Redis acknowledges
147        // before the write is durable or replicated.
148        assert!(QueueDriver::Redis.may_lose_an_accepted_job());
149
150        assert!(!QueueDriver::Database.may_lose_an_accepted_job());
151        assert!(!QueueDriver::Sqs.may_lose_an_accepted_job());
152        // `acks=all`, so the push returns once the replicas have it — which is
153        // a claim about a replicated cluster, as the doc comment says.
154        assert!(!QueueDriver::Kafka.may_lose_an_accepted_job());
155
156        // Memory loses everything, but `survives_a_restart` already says so —
157        // this predicate is for the ones that look safe.
158        assert!(!QueueDriver::Memory.may_lose_an_accepted_job());
159
160        // Sync has already run the job by the time the process exits, which is
161        // not the same as surviving — there was nothing left to survive.
162        assert!(!QueueDriver::Sync.survives_a_restart());
163    }
164}