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}