1use std::{marker::PhantomData, path::Path, sync::Mutex};
2
3use async_trait::async_trait;
4use fjall::{Database, Keyspace, KeyspaceCreateOptions, PersistMode};
5use serde::{Serialize, de::DeserializeOwned};
6use uuid::Uuid;
7
8use crate::{Task, TaskId, TaskQueue, TaskQueueError, TaskSerializer};
9
10pub struct FjallTaskQueue<D, S>
18where
19 S: TaskSerializer,
20{
21 db: Database,
22 tasks: Keyspace,
23 queue: Keyspace,
24 inflight: Keyspace,
25 dlq: Keyspace,
26 lock: Mutex<()>,
28 _marker: PhantomData<(D, S)>,
29}
30
31impl<D, S> FjallTaskQueue<D, S>
32where
33 S: TaskSerializer,
34{
35 pub fn open(path: impl AsRef<Path>) -> Result<Self, TaskQueueError> {
36 let db = Database::builder(path).open()?;
37 let tasks = db.keyspace("tasks", KeyspaceCreateOptions::default)?;
38 let queue = db.keyspace("queue", KeyspaceCreateOptions::default)?;
39 let inflight = db.keyspace("inflight", KeyspaceCreateOptions::default)?;
40 let dlq = db.keyspace("dlq", KeyspaceCreateOptions::default)?;
41
42 Ok(Self {
43 db,
44 tasks,
45 queue,
46 inflight,
47 dlq,
48 lock: Mutex::new(()),
49 _marker: PhantomData,
50 })
51 }
52
53 fn task_id_to_bytes(task_id: &TaskId) -> [u8; 16] {
54 *task_id.get().as_bytes()
55 }
56
57 fn task_id_from_bytes(bytes: &[u8]) -> Result<TaskId, TaskQueueError> {
58 let uuid = Uuid::from_slice(bytes).map_err(|e| {
59 TaskQueueError::QueueError(format!("Invalid task id bytes: {e}"))
60 })?;
61 Ok(TaskId::from_uuid(uuid))
62 }
63}
64
65#[async_trait]
66impl<D, S> TaskQueue<D> for FjallTaskQueue<D, S>
67where
68 D: std::fmt::Debug
69 + Clone
70 + Serialize
71 + DeserializeOwned
72 + Send
73 + Sync
74 + 'static,
75 S: TaskSerializer + Send + Sync,
76{
77 async fn push(&self, task: &Task<D>) -> Result<(), TaskQueueError> {
78 let _guard = self.lock.lock().unwrap();
79 let task_bytes = S::serialize_task(task)?;
80
81 let task_id_bytes = Self::task_id_to_bytes(&task.task_id);
82
83 self.tasks.insert(task_id_bytes, &task_bytes)?;
84 self.queue.insert(task_id_bytes, &[] as &[u8])?;
85 self.inflight.remove(task_id_bytes)?;
87
88 self.db.persist(PersistMode::SyncAll)?;
90 Ok(())
91 }
92
93 async fn pop(&self) -> Result<Task<D>, TaskQueueError> {
94 let _guard = self.lock.lock().unwrap();
95
96 let Some(entry) = self.queue.first_key_value() else {
97 return Err(TaskQueueError::QueueEmpty);
98 };
99
100 let (task_id_bytes, _) = entry.into_inner()?;
101 let task_id_bytes = task_id_bytes.to_vec();
102 self.queue.remove(task_id_bytes.clone())?;
103 self.inflight.insert(task_id_bytes.clone(), &[] as &[u8])?;
104 self.db.persist(PersistMode::SyncAll)?;
105
106 let task_id = Self::task_id_from_bytes(&task_id_bytes)?;
107 let task_bytes = self
108 .tasks
109 .get(task_id_bytes)?
110 .ok_or(TaskQueueError::TaskNotFound(task_id))?;
111
112 S::deserialize_task(&task_bytes)
113 }
114
115 async fn ack(&self, task_id: &TaskId) -> Result<(), TaskQueueError> {
116 let _guard = self.lock.lock().unwrap();
117 let task_id_bytes = Self::task_id_to_bytes(task_id);
118 self.tasks.remove(task_id_bytes)?;
119 self.inflight.remove(task_id_bytes)?;
120 self.db.persist(PersistMode::SyncAll)?;
121 Ok(())
122 }
123
124 async fn nack(&self, task: &Task<D>) -> Result<(), TaskQueueError> {
125 let _guard = self.lock.lock().unwrap();
126 let task_id_bytes = Self::task_id_to_bytes(&task.task_id);
127 let task_bytes = S::serialize_task(task)?;
128
129 self.dlq.insert(task_id_bytes, &task_bytes)?;
130 self.tasks.remove(task_id_bytes)?;
131 self.inflight.remove(task_id_bytes)?;
132 self.db.persist(PersistMode::SyncAll)?;
133 Ok(())
134 }
135
136 async fn set(&self, task: &Task<D>) -> Result<(), TaskQueueError> {
137 let _guard = self.lock.lock().unwrap();
138 let task_id_bytes = Self::task_id_to_bytes(&task.task_id);
139 let task_bytes = S::serialize_task(task)?;
140 self.tasks.insert(task_id_bytes, &task_bytes)?;
141 Ok(())
142 }
143
144 async fn recover_inflight(&self) -> Result<u64, TaskQueueError> {
145 let _guard = self.lock.lock().unwrap();
146
147 let mut recovered = 0u64;
148 let mut keys = Vec::new();
149 for item in self.inflight.iter() {
150 let (key, _) = item.into_inner()?;
151 keys.push(key.to_vec());
152 }
153
154 for key in keys {
155 self.queue.insert(key.clone(), &[] as &[u8])?;
156 self.inflight.remove(key)?;
157 recovered += 1;
158 }
159
160 if recovered > 0 {
161 self.db.persist(PersistMode::SyncAll)?;
162 }
163 Ok(recovered)
164 }
165}
166
167impl<D, S> std::fmt::Debug for FjallTaskQueue<D, S>
168where
169 S: TaskSerializer,
170{
171 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
172 f.debug_struct("FjallTaskQueue").finish()
173 }
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179 use crate::JsonSerializer;
180 use serde::{Deserialize, Serialize};
181 use tempfile::tempdir;
182
183 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
184 struct TestData {
185 value: u32,
186 }
187
188 fn make_queue() -> (tempfile::TempDir, FjallTaskQueue<TestData, JsonSerializer>)
189 {
190 let dir = tempdir().unwrap();
191 let queue = FjallTaskQueue::open(dir.path()).unwrap();
192 (dir, queue)
193 }
194
195 #[tokio::test]
196 async fn push_and_pop() -> Result<(), TaskQueueError> {
197 let (_dir, queue) = make_queue();
198 let task = Task::new(TestData { value: 7 });
199
200 queue.push(&task).await?;
201 let popped = queue.pop().await?;
202 assert_eq!(popped.payload, task.payload);
203 Ok(())
204 }
205
206 #[tokio::test]
207 async fn queue_empty() -> Result<(), TaskQueueError> {
208 let (_dir, queue) = make_queue();
209 let res = queue.pop().await;
210 assert!(matches!(res, Err(TaskQueueError::QueueEmpty)));
211 Ok(())
212 }
213
214 #[tokio::test]
215 async fn ack_removes_task() -> Result<(), TaskQueueError> {
216 let (_dir, queue) = make_queue();
217 let task = Task::new(TestData { value: 1 });
218
219 queue.push(&task).await?;
220 let popped = queue.pop().await?;
221 queue.ack(&popped.task_id).await?;
222
223 let res = queue.pop().await;
224 assert!(matches!(res, Err(TaskQueueError::QueueEmpty)));
225 Ok(())
226 }
227
228 #[tokio::test]
229 async fn nack_moves_to_dlq() -> Result<(), TaskQueueError> {
230 let (_dir, queue) = make_queue();
231 let task = Task::new(TestData { value: 99 });
232
233 queue.push(&task).await?;
234 let mut popped = queue.pop().await?;
235 popped.set_retry("fail");
236 queue.nack(&popped).await?;
237
238 let stored = queue
239 .dlq
240 .get(
241 FjallTaskQueue::<TestData, JsonSerializer>::task_id_to_bytes(
242 &popped.task_id,
243 ),
244 )?
245 .map(|bytes| JsonSerializer::deserialize_task::<TestData>(&bytes));
246 assert!(matches!(stored, Some(Ok(_))));
247 Ok(())
248 }
249
250 #[tokio::test]
251 async fn set_updates_task() -> Result<(), TaskQueueError> {
252 let (_dir, queue) = make_queue();
253 let mut task = Task::new(TestData { value: 3 });
254 queue.push(&task).await?;
255
256 task.payload.value = 5;
257 queue.set(&task).await?;
258
259 let popped = queue.pop().await?;
260 assert_eq!(popped.payload.value, 5);
261 Ok(())
262 }
263
264 fn inflight_contains<S: TaskSerializer>(
265 queue: &FjallTaskQueue<TestData, S>,
266 task_id: &TaskId,
267 ) -> bool {
268 let key = FjallTaskQueue::<TestData, S>::task_id_to_bytes(task_id);
269 queue
270 .inflight
271 .get(key)
272 .map(|v| v.is_some())
273 .unwrap_or(false)
274 }
275
276 #[tokio::test]
277 async fn pop_moves_to_inflight() -> Result<(), TaskQueueError> {
278 let (_dir, queue) = make_queue();
279 let task = Task::new(TestData { value: 11 });
280
281 queue.push(&task).await?;
282 let popped = queue.pop().await?;
283
284 assert!(inflight_contains(&queue, &popped.task_id));
285 Ok(())
286 }
287
288 #[tokio::test]
289 async fn ack_clears_inflight() -> Result<(), TaskQueueError> {
290 let (_dir, queue) = make_queue();
291 let task = Task::new(TestData { value: 12 });
292
293 queue.push(&task).await?;
294 let popped = queue.pop().await?;
295 queue.ack(&popped.task_id).await?;
296
297 assert!(!inflight_contains(&queue, &popped.task_id));
298 Ok(())
299 }
300
301 #[tokio::test]
302 async fn nack_clears_inflight() -> Result<(), TaskQueueError> {
303 let (_dir, queue) = make_queue();
304 let task = Task::new(TestData { value: 13 });
305
306 queue.push(&task).await?;
307 let mut popped = queue.pop().await?;
308 popped.set_retry("fail");
309 queue.nack(&popped).await?;
310
311 assert!(!inflight_contains(&queue, &popped.task_id));
312 Ok(())
313 }
314
315 #[tokio::test]
316 async fn push_retry_clears_inflight() -> Result<(), TaskQueueError> {
317 let (_dir, queue) = make_queue();
318 let mut task = Task::new(TestData { value: 14 });
319
320 queue.push(&task).await?;
321 let popped = queue.pop().await?;
322 assert!(inflight_contains(&queue, &popped.task_id));
323
324 task.set_retry("transient");
325 queue.push(&task).await?;
326
327 assert!(!inflight_contains(&queue, &task.task_id));
328 Ok(())
329 }
330
331 #[tokio::test]
332 async fn recover_inflight_moves_back_to_queue() -> Result<(), TaskQueueError> {
333 let dir = tempdir().unwrap();
334 let task_id = {
335 let queue: FjallTaskQueue<TestData, JsonSerializer> =
336 FjallTaskQueue::open(dir.path())?;
337 let task = Task::new(TestData { value: 99 });
338 queue.push(&task).await?;
339 let popped = queue.pop().await?;
340 assert!(inflight_contains(&queue, &popped.task_id));
341 popped.task_id
342 };
343
344 let queue: FjallTaskQueue<TestData, JsonSerializer> =
346 FjallTaskQueue::open(dir.path())?;
347 let recovered = queue.recover_inflight().await?;
348 assert_eq!(recovered, 1);
349
350 let re_popped = queue.pop().await?;
351 assert_eq!(re_popped.task_id, task_id);
352 Ok(())
353 }
354}