1use std::{collections::HashSet, 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::{HasTagKey, Task, TaskId, TaskQueue, TaskQueueError, TaskSerializer};
9
10pub struct FjallRoundRobinTaskQueue<D, S>
26where
27 D: HasTagKey,
28 S: TaskSerializer,
29{
30 db: Database,
31 tasks: Keyspace,
32 queue: Keyspace,
33 inflight: Keyspace,
34 dlq: Keyspace,
35 state: Mutex<RotationState>,
36 _marker: PhantomData<(D, S)>,
37}
38
39#[derive(Debug, Default)]
40struct RotationState {
41 active: Vec<String>,
42 cursor: usize,
43 present: HashSet<String>,
44}
45
46impl RotationState {
47 fn ensure(&mut self, tag: &str) {
48 if self.present.insert(tag.to_string()) {
49 self.active.push(tag.to_string());
50 }
51 }
52}
53
54impl<D, S> FjallRoundRobinTaskQueue<D, S>
55where
56 D: HasTagKey,
57 S: TaskSerializer,
58{
59 pub fn open(path: impl AsRef<Path>) -> Result<Self, TaskQueueError> {
60 let db = Database::builder(path).open()?;
61 let tasks = db.keyspace("tasks", KeyspaceCreateOptions::default)?;
62 let queue = db.keyspace("queue", KeyspaceCreateOptions::default)?;
63 let inflight = db.keyspace("inflight", KeyspaceCreateOptions::default)?;
64 let dlq = db.keyspace("dlq", KeyspaceCreateOptions::default)?;
65
66 let state = Self::rebuild_state(&queue)?;
67
68 Ok(Self {
69 db,
70 tasks,
71 queue,
72 inflight,
73 dlq,
74 state: Mutex::new(state),
75 _marker: PhantomData,
76 })
77 }
78
79 fn rebuild_state(queue: &Keyspace) -> Result<RotationState, TaskQueueError> {
80 let mut present: HashSet<String> = HashSet::new();
81 let mut active: Vec<String> = Vec::new();
82 for item in queue.iter() {
83 let (key, _) = item.into_inner()?;
84 let tag = decode_tag_from_key(&key)?.to_string();
85 if present.insert(tag.clone()) {
86 active.push(tag);
87 }
88 }
89 active.sort();
91 Ok(RotationState {
92 active,
93 cursor: 0,
94 present,
95 })
96 }
97
98 fn task_id_to_bytes(task_id: &TaskId) -> [u8; 16] {
99 *task_id.get().as_bytes()
100 }
101}
102
103#[async_trait]
104impl<D, S> TaskQueue<D> for FjallRoundRobinTaskQueue<D, S>
105where
106 D: HasTagKey
107 + std::fmt::Debug
108 + Clone
109 + Serialize
110 + DeserializeOwned
111 + Send
112 + Sync
113 + 'static,
114 S: TaskSerializer + Send + Sync,
115{
116 async fn push(&self, task: &Task<D>) -> Result<(), TaskQueueError> {
117 let tag = task.payload.get_tag_value().to_string();
118 let task_id_bytes = Self::task_id_to_bytes(&task.task_id);
119 let task_bytes = S::serialize_task(task)?;
120 let queue_key = encode_queue_key(&tag, &task_id_bytes);
121
122 let mut state = self.state.lock().unwrap();
123 self.tasks.insert(task_id_bytes, &task_bytes)?;
124 self.queue.insert(queue_key, &[] as &[u8])?;
125 self.inflight.remove(task_id_bytes)?;
126 self.db.persist(PersistMode::SyncAll)?;
127
128 state.ensure(&tag);
129 Ok(())
130 }
131
132 async fn pop(&self) -> Result<Task<D>, TaskQueueError> {
133 let mut state = self.state.lock().unwrap();
134
135 if state.active.is_empty() {
136 return Err(TaskQueueError::QueueEmpty);
137 }
138 let n = state.active.len();
139 for step in 0..n {
140 let idx = (state.cursor + step) % n;
141 let tag = state.active[idx].clone();
142 let prefix = encode_tag_prefix(&tag);
143
144 let Some(guard) = self.queue.prefix(&prefix).next() else {
145 continue;
148 };
149 let (key, _) = guard.into_inner()?;
150
151 let task_id = extract_task_id_from_key(&key)?;
152 let task_id_bytes = Self::task_id_to_bytes(&task_id);
153
154 self.queue.remove(key.as_ref())?;
155 self.inflight.insert(task_id_bytes, encode_tag_blob(&tag))?;
156 self.db.persist(PersistMode::SyncAll)?;
157
158 let task_bytes = self
159 .tasks
160 .get(task_id_bytes)?
161 .ok_or(TaskQueueError::TaskNotFound(task_id))?;
162 let task = S::deserialize_task::<D>(&task_bytes)?;
163
164 state.cursor = (idx + 1) % n;
165 return Ok(task);
166 }
167 Err(TaskQueueError::QueueEmpty)
168 }
169
170 async fn ack(&self, task_id: &TaskId) -> Result<(), TaskQueueError> {
171 let _guard = self.state.lock().unwrap();
172 let task_id_bytes = Self::task_id_to_bytes(task_id);
173 self.tasks.remove(task_id_bytes)?;
174 self.inflight.remove(task_id_bytes)?;
175 self.db.persist(PersistMode::SyncAll)?;
176 Ok(())
177 }
178
179 async fn nack(&self, task: &Task<D>) -> Result<(), TaskQueueError> {
180 let _guard = self.state.lock().unwrap();
181 let task_id_bytes = Self::task_id_to_bytes(&task.task_id);
182 let task_bytes = S::serialize_task(task)?;
183
184 self.dlq.insert(task_id_bytes, &task_bytes)?;
185 self.tasks.remove(task_id_bytes)?;
186 self.inflight.remove(task_id_bytes)?;
187 self.db.persist(PersistMode::SyncAll)?;
188 Ok(())
189 }
190
191 async fn set(&self, task: &Task<D>) -> Result<(), TaskQueueError> {
192 let _guard = self.state.lock().unwrap();
193 let task_id_bytes = Self::task_id_to_bytes(&task.task_id);
194 let task_bytes = S::serialize_task(task)?;
195 self.tasks.insert(task_id_bytes, &task_bytes)?;
196 Ok(())
197 }
198
199 async fn recover_inflight(&self) -> Result<u64, TaskQueueError> {
200 let mut state = self.state.lock().unwrap();
201
202 let mut entries: Vec<([u8; 16], String)> = Vec::new();
203 for item in self.inflight.iter() {
204 let (key, value) = item.into_inner()?;
205 if key.len() != 16 {
206 return Err(TaskQueueError::QueueError(
207 "inflight key is not a 16-byte task id".into(),
208 ));
209 }
210 let mut id = [0u8; 16];
211 id.copy_from_slice(&key);
212 let tag = decode_tag_blob(&value)?.to_string();
213 entries.push((id, tag));
214 }
215
216 let mut recovered = 0u64;
217 for (task_id_bytes, tag) in entries {
218 self.queue
221 .insert(encode_queue_key(&tag, &task_id_bytes), &[] as &[u8])?;
222 self.inflight.remove(&task_id_bytes[..])?;
223 state.ensure(&tag);
224 recovered += 1;
225 }
226
227 if recovered > 0 {
228 self.db.persist(PersistMode::SyncAll)?;
229 }
230 Ok(recovered)
231 }
232}
233
234impl<D, S> std::fmt::Debug for FjallRoundRobinTaskQueue<D, S>
235where
236 D: HasTagKey,
237 S: TaskSerializer,
238{
239 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
240 f.debug_struct("FjallRoundRobinTaskQueue").finish()
241 }
242}
243
244fn encode_queue_key(tag: &str, task_id_bytes: &[u8; 16]) -> Vec<u8> {
245 let tag_bytes = tag.as_bytes();
246 let len =
247 u16::try_from(tag_bytes.len()).expect("tag too long (max 65535 bytes)");
248 let mut key = Vec::with_capacity(2 + tag_bytes.len() + 16);
249 key.extend_from_slice(&len.to_be_bytes());
250 key.extend_from_slice(tag_bytes);
251 key.extend_from_slice(task_id_bytes);
252 key
253}
254
255fn encode_tag_prefix(tag: &str) -> Vec<u8> {
256 let tag_bytes = tag.as_bytes();
257 let len =
258 u16::try_from(tag_bytes.len()).expect("tag too long (max 65535 bytes)");
259 let mut prefix = Vec::with_capacity(2 + tag_bytes.len());
260 prefix.extend_from_slice(&len.to_be_bytes());
261 prefix.extend_from_slice(tag_bytes);
262 prefix
263}
264
265fn encode_tag_blob(tag: &str) -> Vec<u8> {
266 encode_tag_prefix(tag)
267}
268
269fn decode_tag_from_key(key: &[u8]) -> Result<&str, TaskQueueError> {
270 if key.len() < 2 {
271 return Err(TaskQueueError::QueueError("queue key too short".into()));
272 }
273 let len = u16::from_be_bytes([key[0], key[1]]) as usize;
274 if key.len() < 2 + len + 16 {
275 return Err(TaskQueueError::QueueError(
276 "queue key shorter than declared tag length".into(),
277 ));
278 }
279 std::str::from_utf8(&key[2..2 + len])
280 .map_err(|e| TaskQueueError::QueueError(format!("invalid tag utf8: {e}")))
281}
282
283fn decode_tag_blob(bytes: &[u8]) -> Result<&str, TaskQueueError> {
284 if bytes.len() < 2 {
285 return Err(TaskQueueError::QueueError("tag blob too short".into()));
286 }
287 let len = u16::from_be_bytes([bytes[0], bytes[1]]) as usize;
288 if bytes.len() != 2 + len {
289 return Err(TaskQueueError::QueueError(
290 "tag blob length mismatch".into(),
291 ));
292 }
293 std::str::from_utf8(&bytes[2..])
294 .map_err(|e| TaskQueueError::QueueError(format!("invalid tag utf8: {e}")))
295}
296
297fn extract_task_id_from_key(key: &[u8]) -> Result<TaskId, TaskQueueError> {
298 if key.len() < 18 {
299 return Err(TaskQueueError::QueueError(
300 "queue key missing task id suffix".into(),
301 ));
302 }
303 let suffix = &key[key.len() - 16..];
304 let uuid = Uuid::from_slice(suffix)
305 .map_err(|e| TaskQueueError::QueueError(format!("invalid uuid: {e}")))?;
306 Ok(TaskId::from_uuid(uuid))
307}