Skip to main content

celers_core/
in_memory_broker.rs

1//! In-memory broker and result backend for local development and testing.
2//!
3//! This module provides fully-featured, dependency-free, in-process
4//! implementations of the [`Broker`](crate::Broker) and
5//! [`ResultStore`] traits. They are intended for local
6//! development, unit/integration tests, and example code where standing up an
7//! external service such as Redis is undesirable.
8//!
9//! Both types are backed by [`tokio`]-synchronised data structures so they can
10//! be shared freely across tasks via [`Arc`](std::sync::Arc) and exercised from
11//! concurrent code exactly like a real broker/backend.
12//!
13//! # Examples
14//!
15//! ## In-memory broker round-trip
16//!
17//! ```
18//! use celers_core::{Broker, InMemoryBroker, SerializedTask};
19//!
20//! # async fn example() -> celers_core::Result<()> {
21//! let broker = InMemoryBroker::new();
22//!
23//! let task = SerializedTask::new("send_email".to_string(), vec![1, 2, 3]);
24//! let id = broker.enqueue(task).await?;
25//!
26//! let msg = broker.dequeue().await?.expect("a message is available");
27//! assert_eq!(msg.task_id(), id);
28//!
29//! // Acknowledge with the receipt handle the broker attached on dequeue.
30//! broker.ack(&id, msg.receipt_handle.as_deref()).await?;
31//! assert_eq!(broker.queue_size().await?, 0);
32//! # Ok(())
33//! # }
34//! # tokio::runtime::Builder::new_current_thread()
35//! #     .enable_all()
36//! #     .build()
37//! #     .unwrap()
38//! #     .block_on(example())
39//! #     .unwrap();
40//! ```
41//!
42//! ## In-memory result backend
43//!
44//! ```
45//! use celers_core::{InMemoryResultBackend, ResultStore, TaskResultValue};
46//! use uuid::Uuid;
47//!
48//! # async fn example() -> celers_core::Result<()> {
49//! let backend = InMemoryResultBackend::new();
50//! let id = Uuid::new_v4();
51//!
52//! backend
53//!     .store_result(id, TaskResultValue::Success(serde_json::json!(42)))
54//!     .await?;
55//! assert!(backend.has_result(id).await?);
56//!
57//! backend.forget(id).await?;
58//! assert!(!backend.has_result(id).await?);
59//! # Ok(())
60//! # }
61//! # tokio::runtime::Builder::new_current_thread()
62//! #     .enable_all()
63//! #     .build()
64//! #     .unwrap()
65//! #     .block_on(example())
66//! #     .unwrap();
67//! ```
68
69use crate::result::{ResultStore, TaskResultValue};
70use crate::state::TaskState;
71use crate::{BrokerMessage, CelersError, Result, SerializedTask, TaskId};
72use std::collections::HashMap;
73use std::sync::atomic::{AtomicU64, Ordering};
74use tokio::sync::{Mutex, Notify};
75use uuid::Uuid;
76
77/// A single entry in the in-memory ready queue.
78///
79/// Entries are ordered first by descending priority and then by ascending
80/// insertion sequence, so that, within a priority class, tasks are delivered in
81/// FIFO order (stable ordering).
82#[derive(Debug)]
83struct QueueEntry {
84    /// The serialized task awaiting delivery.
85    task: SerializedTask,
86    /// Monotonically increasing sequence number used as a FIFO tie-breaker.
87    seq: u64,
88}
89
90/// Internal queue state guarded by a single [`tokio::sync::Mutex`].
91#[derive(Debug, Default)]
92struct BrokerState {
93    /// Tasks that are ready to be delivered, kept sorted on every push.
94    ready: Vec<QueueEntry>,
95    /// Tasks that have been delivered but not yet acknowledged, keyed by their
96    /// receipt handle. Used to support `ack`/`reject` semantics.
97    in_flight: HashMap<String, SerializedTask>,
98    /// Task IDs that have been requested to be cancelled. A cancelled task is
99    /// dropped at dequeue time (for ready tasks) or refused requeue (for
100    /// in-flight tasks).
101    cancelled: HashMap<TaskId, ()>,
102}
103
104impl BrokerState {
105    /// Insert a task into the ready queue, preserving the priority + FIFO
106    /// ordering invariant.
107    fn push_ready(&mut self, task: SerializedTask, seq: u64) {
108        let entry = QueueEntry { task, seq };
109        // Find the insertion point that keeps `ready` sorted by descending
110        // priority and ascending sequence. `partition_point` gives the first
111        // index for which the predicate is false.
112        let idx = self.ready.partition_point(|existing| {
113            existing.task.metadata.priority > entry.task.metadata.priority
114                || (existing.task.metadata.priority == entry.task.metadata.priority
115                    && existing.seq < entry.seq)
116        });
117        self.ready.insert(idx, entry);
118    }
119}
120
121/// A fully in-memory implementation of the [`Broker`](crate::Broker) trait.
122///
123/// This broker keeps all state in process behind a [`tokio::sync::Mutex`] and
124/// supports the full broker contract: priority-ordered enqueue/dequeue,
125/// receipt-handle based [`ack`](crate::Broker::ack) /
126/// [`reject`](crate::Broker::reject) with optional requeue,
127/// [`queue_size`](crate::Broker::queue_size), and
128/// [`cancel`](crate::Broker::cancel).
129///
130/// Higher-priority tasks are delivered first; within the same priority tasks are
131/// delivered in FIFO (insertion) order.
132///
133/// The broker is cheap to [`Clone`]; clones share the same underlying queue.
134#[derive(Debug, Default)]
135pub struct InMemoryBroker {
136    /// Shared mutable state.
137    state: Mutex<BrokerState>,
138    /// Notifier used to wake waiters blocked in [`InMemoryBroker::dequeue`].
139    notify: Notify,
140    /// Monotonic sequence counter for FIFO tie-breaking.
141    seq: AtomicU64,
142}
143
144impl InMemoryBroker {
145    /// Create a new, empty in-memory broker.
146    #[must_use]
147    pub fn new() -> Self {
148        Self {
149            state: Mutex::new(BrokerState::default()),
150            notify: Notify::new(),
151            seq: AtomicU64::new(0),
152        }
153    }
154
155    /// Number of tasks that have been delivered but not yet acknowledged.
156    ///
157    /// Primarily useful for tests and diagnostics.
158    pub async fn in_flight_len(&self) -> usize {
159        self.state.lock().await.in_flight.len()
160    }
161
162    /// Returns `true` if there are no ready and no in-flight tasks.
163    pub async fn is_empty(&self) -> bool {
164        let guard = self.state.lock().await;
165        guard.ready.is_empty() && guard.in_flight.is_empty()
166    }
167
168    /// Remove every task from the broker (ready, in-flight, and cancellation
169    /// markers). Mainly intended for resetting state between tests.
170    pub async fn clear(&self) {
171        let mut guard = self.state.lock().await;
172        guard.ready.clear();
173        guard.in_flight.clear();
174        guard.cancelled.clear();
175    }
176
177    /// Pop the next deliverable ready entry, skipping (and discarding) any
178    /// task that has been cancelled. Returns `None` if the ready queue is
179    /// empty after skipping cancelled tasks.
180    fn pop_deliverable(state: &mut BrokerState) -> Option<SerializedTask> {
181        while !state.ready.is_empty() {
182            let entry = state.ready.remove(0);
183            if state.cancelled.remove(&entry.task.metadata.id).is_some() {
184                // Task was cancelled before delivery; drop it and continue.
185                continue;
186            }
187            return Some(entry.task);
188        }
189        None
190    }
191
192    /// Non-blocking dequeue: if a deliverable task is available, move it
193    /// in-flight and return a [`BrokerMessage`] for it; otherwise return
194    /// `None` immediately without waiting. Used by both [`Self::dequeue`] (which
195    /// then waits on a miss) and the batch dequeue (which must never block).
196    fn try_dequeue_locked(state: &mut BrokerState) -> Option<BrokerMessage> {
197        let task = Self::pop_deliverable(state)?;
198        let receipt = Uuid::new_v4().to_string();
199        state.in_flight.insert(receipt.clone(), task.clone());
200        Some(BrokerMessage::with_receipt_handle(task, receipt))
201    }
202}
203
204#[async_trait::async_trait]
205impl crate::Broker for InMemoryBroker {
206    async fn enqueue(&self, task: SerializedTask) -> Result<TaskId> {
207        let task_id = task.metadata.id;
208        let seq = self.seq.fetch_add(1, Ordering::Relaxed);
209        {
210            let mut guard = self.state.lock().await;
211            guard.push_ready(task, seq);
212        }
213        // Wake a single waiter (if any) that may be blocked in `dequeue`.
214        self.notify.notify_one();
215        Ok(task_id)
216    }
217
218    async fn dequeue(&self) -> Result<Option<BrokerMessage>> {
219        loop {
220            // Register interest *before* checking the queue to avoid a lost
221            // wakeup between the check and the await.
222            let notified = self.notify.notified();
223            {
224                let mut guard = self.state.lock().await;
225                if let Some(msg) = Self::try_dequeue_locked(&mut guard) {
226                    return Ok(Some(msg));
227                }
228            }
229            // Queue empty: wait until something is enqueued, then retry.
230            notified.await;
231        }
232    }
233
234    async fn ack(&self, _task_id: &TaskId, receipt_handle: Option<&str>) -> Result<()> {
235        let Some(handle) = receipt_handle else {
236            return Err(CelersError::Broker(
237                "in-memory broker ack requires a receipt handle".to_string(),
238            ));
239        };
240        let mut guard = self.state.lock().await;
241        if guard.in_flight.remove(handle).is_none() {
242            return Err(CelersError::Broker(format!(
243                "unknown receipt handle on ack: {handle}"
244            )));
245        }
246        Ok(())
247    }
248
249    async fn reject(
250        &self,
251        _task_id: &TaskId,
252        receipt_handle: Option<&str>,
253        requeue: bool,
254    ) -> Result<()> {
255        let Some(handle) = receipt_handle else {
256            return Err(CelersError::Broker(
257                "in-memory broker reject requires a receipt handle".to_string(),
258            ));
259        };
260        let task = {
261            let mut guard = self.state.lock().await;
262            match guard.in_flight.remove(handle) {
263                Some(task) => task,
264                None => {
265                    return Err(CelersError::Broker(format!(
266                        "unknown receipt handle on reject: {handle}"
267                    )));
268                }
269            }
270        };
271
272        if !requeue {
273            return Ok(());
274        }
275
276        let task_id = task.metadata.id;
277        let seq = self.seq.fetch_add(1, Ordering::Relaxed);
278        {
279            let mut guard = self.state.lock().await;
280            // Honour a cancellation that arrived while the task was in flight.
281            if guard.cancelled.remove(&task_id).is_some() {
282                return Ok(());
283            }
284            guard.push_ready(task, seq);
285        }
286        self.notify.notify_one();
287        Ok(())
288    }
289
290    async fn queue_size(&self) -> Result<usize> {
291        Ok(self.state.lock().await.ready.len())
292    }
293
294    async fn cancel(&self, task_id: &TaskId) -> Result<bool> {
295        let mut guard = self.state.lock().await;
296        // Try to remove the task directly from the ready queue first.
297        if let Some(pos) = guard
298            .ready
299            .iter()
300            .position(|entry| entry.task.metadata.id == *task_id)
301        {
302            guard.ready.remove(pos);
303            // Also drop any stale cancellation marker for this id.
304            guard.cancelled.remove(task_id);
305            return Ok(true);
306        }
307
308        // If the task is currently in flight, record a cancellation marker so
309        // that a subsequent requeue is suppressed. Report success because the
310        // task is known to the broker.
311        let in_flight = guard
312            .in_flight
313            .values()
314            .any(|task| task.metadata.id == *task_id);
315        if in_flight {
316            guard.cancelled.insert(*task_id, ());
317            return Ok(true);
318        }
319
320        Ok(false)
321    }
322
323    async fn enqueue_batch(&self, tasks: Vec<SerializedTask>) -> Result<Vec<TaskId>> {
324        if tasks.is_empty() {
325            return Ok(Vec::new());
326        }
327        let mut ids = Vec::with_capacity(tasks.len());
328        {
329            let mut guard = self.state.lock().await;
330            for task in tasks {
331                ids.push(task.metadata.id);
332                let seq = self.seq.fetch_add(1, Ordering::Relaxed);
333                guard.push_ready(task, seq);
334            }
335        }
336        // Wake potentially several waiters.
337        self.notify.notify_waiters();
338        Ok(ids)
339    }
340
341    async fn dequeue_batch(&self, count: usize) -> Result<Vec<BrokerMessage>> {
342        // Unlike the trait default (which calls the *blocking* `dequeue` in a
343        // loop), this override drains up to `count` immediately-available
344        // messages and returns straight away when the queue runs dry, so it
345        // never blocks waiting for more tasks to arrive.
346        let mut messages = Vec::with_capacity(count.min(64));
347        let mut guard = self.state.lock().await;
348        for _ in 0..count {
349            match Self::try_dequeue_locked(&mut guard) {
350                Some(msg) => messages.push(msg),
351                None => break,
352            }
353        }
354        Ok(messages)
355    }
356}
357
358/// Stored state for a single task result in [`InMemoryResultBackend`].
359#[derive(Debug, Clone)]
360struct StoredResult {
361    /// The most recently stored result value.
362    value: TaskResultValue,
363}
364
365/// Internal state for [`InMemoryResultBackend`].
366#[derive(Debug, Default)]
367struct BackendState {
368    /// Live results keyed by task ID.
369    results: HashMap<TaskId, StoredResult>,
370    /// Tombstones recorded for forgotten results.
371    tombstones: HashMap<TaskId, crate::ResultTombstone>,
372}
373
374/// A fully in-memory implementation of the [`ResultStore`]
375/// trait.
376///
377/// All results are stored in process behind a [`tokio::sync::Mutex`]. The
378/// backend implements the complete `ResultStore` contract — `store_result`,
379/// `get_result`, `get_state`, `forget`, and `has_result` — and additionally
380/// provides native support for the optional tombstone hooks so that a forgotten
381/// result can be distinguished from one that never existed.
382///
383/// The backend is cheap to [`Clone`]; clones share the same underlying storage.
384#[derive(Debug, Default)]
385pub struct InMemoryResultBackend {
386    /// Shared mutable state.
387    state: Mutex<BackendState>,
388}
389
390impl InMemoryResultBackend {
391    /// Create a new, empty in-memory result backend.
392    #[must_use]
393    pub fn new() -> Self {
394        Self {
395            state: Mutex::new(BackendState::default()),
396        }
397    }
398
399    /// Number of live (non-forgotten) results currently stored.
400    pub async fn len(&self) -> usize {
401        self.state.lock().await.results.len()
402    }
403
404    /// Returns `true` if no live results are stored.
405    pub async fn is_empty(&self) -> bool {
406        self.state.lock().await.results.is_empty()
407    }
408
409    /// Remove all results and tombstones. Mainly intended for tests.
410    pub async fn clear(&self) {
411        let mut guard = self.state.lock().await;
412        guard.results.clear();
413        guard.tombstones.clear();
414    }
415
416    /// Derive the [`TaskState`] that corresponds to a stored result value.
417    fn state_for(value: &TaskResultValue) -> TaskState {
418        match value {
419            TaskResultValue::Pending => TaskState::Pending,
420            TaskResultValue::Received => TaskState::Received,
421            TaskResultValue::Started => TaskState::Running,
422            TaskResultValue::Success(v) => {
423                // Best-effort serialization of the success payload into the
424                // state's byte buffer; an unserializable value degrades to an
425                // empty buffer rather than failing the state query.
426                let bytes = serde_json::to_vec(v).unwrap_or_default();
427                TaskState::Succeeded(bytes)
428            }
429            TaskResultValue::Failure { error, .. } => TaskState::Failed(error.clone()),
430            TaskResultValue::Revoked => TaskState::Revoked,
431            TaskResultValue::Retry { attempt, .. } => TaskState::Retrying(*attempt),
432            TaskResultValue::Rejected { .. } => TaskState::Rejected,
433        }
434    }
435}
436
437#[async_trait::async_trait]
438impl ResultStore for InMemoryResultBackend {
439    async fn store_result(&self, task_id: TaskId, result: TaskResultValue) -> Result<()> {
440        let mut guard = self.state.lock().await;
441        // Storing a fresh result clears any prior tombstone for that task.
442        guard.tombstones.remove(&task_id);
443        guard
444            .results
445            .insert(task_id, StoredResult { value: result });
446        Ok(())
447    }
448
449    async fn get_result(&self, task_id: TaskId) -> Result<Option<TaskResultValue>> {
450        Ok(self
451            .state
452            .lock()
453            .await
454            .results
455            .get(&task_id)
456            .map(|stored| stored.value.clone()))
457    }
458
459    async fn get_state(&self, task_id: TaskId) -> Result<TaskState> {
460        let guard = self.state.lock().await;
461        match guard.results.get(&task_id) {
462            Some(stored) => Ok(Self::state_for(&stored.value)),
463            None => Ok(TaskState::Pending),
464        }
465    }
466
467    async fn forget(&self, task_id: TaskId) -> Result<()> {
468        self.state.lock().await.results.remove(&task_id);
469        Ok(())
470    }
471
472    async fn has_result(&self, task_id: TaskId) -> Result<bool> {
473        Ok(self.state.lock().await.results.contains_key(&task_id))
474    }
475
476    async fn store_tombstone(&self, tombstone: crate::ResultTombstone) -> Result<()> {
477        self.state
478            .lock()
479            .await
480            .tombstones
481            .insert(tombstone.task_id, tombstone);
482        Ok(())
483    }
484
485    async fn get_tombstone(&self, task_id: TaskId) -> Result<Option<crate::ResultTombstone>> {
486        Ok(self.state.lock().await.tombstones.get(&task_id).cloned())
487    }
488}
489
490#[cfg(test)]
491mod tests {
492    use super::*;
493    use crate::Broker;
494    use serde_json::json;
495
496    fn task(name: &str) -> SerializedTask {
497        SerializedTask::new(name.to_string(), vec![1, 2, 3])
498    }
499
500    #[tokio::test]
501    async fn enqueue_dequeue_ack_round_trip() {
502        let broker = InMemoryBroker::new();
503        let t = task("a");
504        let id = broker.enqueue(t).await.unwrap();
505        assert_eq!(broker.queue_size().await.unwrap(), 1);
506
507        let msg = broker.dequeue().await.unwrap().expect("message");
508        assert_eq!(msg.task_id(), id);
509        assert!(msg.has_receipt_handle());
510        // Dequeued task leaves the ready queue but is tracked in flight.
511        assert_eq!(broker.queue_size().await.unwrap(), 0);
512        assert_eq!(broker.in_flight_len().await, 1);
513
514        broker
515            .ack(&id, msg.receipt_handle.as_deref())
516            .await
517            .unwrap();
518        assert_eq!(broker.in_flight_len().await, 0);
519        assert!(broker.is_empty().await);
520    }
521
522    #[tokio::test]
523    async fn ack_without_handle_errors() {
524        let broker = InMemoryBroker::new();
525        let id = broker.enqueue(task("a")).await.unwrap();
526        let _ = broker.dequeue().await.unwrap().unwrap();
527        let err = broker.ack(&id, None).await.unwrap_err();
528        assert!(err.is_broker());
529    }
530
531    #[tokio::test]
532    async fn ack_unknown_handle_errors() {
533        let broker = InMemoryBroker::new();
534        let id = broker.enqueue(task("a")).await.unwrap();
535        let err = broker.ack(&id, Some("does-not-exist")).await.unwrap_err();
536        assert!(err.is_broker());
537    }
538
539    #[tokio::test]
540    async fn fifo_order_within_same_priority() {
541        let broker = InMemoryBroker::new();
542        let id1 = broker.enqueue(task("first")).await.unwrap();
543        let id2 = broker.enqueue(task("second")).await.unwrap();
544        let id3 = broker.enqueue(task("third")).await.unwrap();
545
546        let m1 = broker.dequeue().await.unwrap().unwrap();
547        let m2 = broker.dequeue().await.unwrap().unwrap();
548        let m3 = broker.dequeue().await.unwrap().unwrap();
549        assert_eq!(m1.task_id(), id1);
550        assert_eq!(m2.task_id(), id2);
551        assert_eq!(m3.task_id(), id3);
552    }
553
554    #[tokio::test]
555    async fn higher_priority_delivered_first() {
556        let broker = InMemoryBroker::new();
557        let low = broker.enqueue(task("low").with_priority(1)).await.unwrap();
558        let high = broker
559            .enqueue(task("high").with_priority(10))
560            .await
561            .unwrap();
562        let mid = broker.enqueue(task("mid").with_priority(5)).await.unwrap();
563
564        assert_eq!(broker.dequeue().await.unwrap().unwrap().task_id(), high);
565        assert_eq!(broker.dequeue().await.unwrap().unwrap().task_id(), mid);
566        assert_eq!(broker.dequeue().await.unwrap().unwrap().task_id(), low);
567    }
568
569    #[tokio::test]
570    async fn priority_then_fifo_combined() {
571        let broker = InMemoryBroker::new();
572        let a = broker.enqueue(task("a").with_priority(5)).await.unwrap();
573        let b = broker.enqueue(task("b").with_priority(5)).await.unwrap();
574        let c = broker.enqueue(task("c").with_priority(9)).await.unwrap();
575
576        // c (priority 9) first, then a and b in FIFO order (priority 5).
577        assert_eq!(broker.dequeue().await.unwrap().unwrap().task_id(), c);
578        assert_eq!(broker.dequeue().await.unwrap().unwrap().task_id(), a);
579        assert_eq!(broker.dequeue().await.unwrap().unwrap().task_id(), b);
580    }
581
582    #[tokio::test]
583    async fn reject_with_requeue_returns_task() {
584        let broker = InMemoryBroker::new();
585        let id = broker.enqueue(task("a")).await.unwrap();
586        let msg = broker.dequeue().await.unwrap().unwrap();
587
588        broker
589            .reject(&id, msg.receipt_handle.as_deref(), true)
590            .await
591            .unwrap();
592        // Task should be back on the ready queue.
593        assert_eq!(broker.queue_size().await.unwrap(), 1);
594        assert_eq!(broker.in_flight_len().await, 0);
595
596        let msg2 = broker.dequeue().await.unwrap().unwrap();
597        assert_eq!(msg2.task_id(), id);
598    }
599
600    #[tokio::test]
601    async fn reject_without_requeue_drops_task() {
602        let broker = InMemoryBroker::new();
603        let id = broker.enqueue(task("a")).await.unwrap();
604        let msg = broker.dequeue().await.unwrap().unwrap();
605
606        broker
607            .reject(&id, msg.receipt_handle.as_deref(), false)
608            .await
609            .unwrap();
610        assert!(broker.is_empty().await);
611    }
612
613    #[tokio::test]
614    async fn cancel_ready_task() {
615        let broker = InMemoryBroker::new();
616        let id = broker.enqueue(task("a")).await.unwrap();
617        assert!(broker.cancel(&id).await.unwrap());
618        assert_eq!(broker.queue_size().await.unwrap(), 0);
619        // Cancelling an unknown task returns false.
620        assert!(!broker.cancel(&id).await.unwrap());
621    }
622
623    #[tokio::test]
624    async fn cancel_in_flight_suppresses_requeue() {
625        let broker = InMemoryBroker::new();
626        let id = broker.enqueue(task("a")).await.unwrap();
627        let msg = broker.dequeue().await.unwrap().unwrap();
628
629        // Cancel while in flight.
630        assert!(broker.cancel(&id).await.unwrap());
631        // Requeue attempt should be suppressed by the cancellation marker.
632        broker
633            .reject(&id, msg.receipt_handle.as_deref(), true)
634            .await
635            .unwrap();
636        assert!(broker.is_empty().await);
637    }
638
639    #[tokio::test]
640    async fn dequeue_empty_returns_when_enqueued() {
641        let broker = std::sync::Arc::new(InMemoryBroker::new());
642        let b2 = broker.clone();
643        let handle = tokio::spawn(async move { b2.dequeue().await });
644
645        // Give the waiter a moment to block, then enqueue.
646        tokio::task::yield_now().await;
647        let id = broker.enqueue(task("late")).await.unwrap();
648
649        let msg = handle.await.unwrap().unwrap().unwrap();
650        assert_eq!(msg.task_id(), id);
651    }
652
653    #[tokio::test]
654    async fn enqueue_batch_and_dequeue_batch() {
655        let broker = InMemoryBroker::new();
656        let tasks = vec![task("a"), task("b"), task("c")];
657        let ids = broker.enqueue_batch(tasks).await.unwrap();
658        assert_eq!(ids.len(), 3);
659        assert_eq!(broker.queue_size().await.unwrap(), 3);
660
661        let msgs = broker.dequeue_batch(10).await.unwrap();
662        assert_eq!(msgs.len(), 3);
663        assert_eq!(broker.queue_size().await.unwrap(), 0);
664    }
665
666    #[tokio::test]
667    async fn clear_empties_broker() {
668        let broker = InMemoryBroker::new();
669        broker.enqueue(task("a")).await.unwrap();
670        let _ = broker.dequeue().await.unwrap();
671        broker.enqueue(task("b")).await.unwrap();
672        broker.clear().await;
673        assert!(broker.is_empty().await);
674    }
675
676    // -------- result backend tests --------
677
678    #[tokio::test]
679    async fn backend_store_get_forget() {
680        let backend = InMemoryResultBackend::new();
681        let id = Uuid::new_v4();
682        assert!(!backend.has_result(id).await.unwrap());
683        assert!(backend.get_result(id).await.unwrap().is_none());
684
685        backend
686            .store_result(id, TaskResultValue::Success(json!({"x": 1})))
687            .await
688            .unwrap();
689        assert!(backend.has_result(id).await.unwrap());
690        assert_eq!(backend.len().await, 1);
691
692        let got = backend.get_result(id).await.unwrap().unwrap();
693        assert!(got.is_successful());
694
695        backend.forget(id).await.unwrap();
696        assert!(!backend.has_result(id).await.unwrap());
697        assert!(backend.is_empty().await);
698    }
699
700    #[tokio::test]
701    async fn backend_get_state_maps_values() {
702        let backend = InMemoryResultBackend::new();
703        let id = Uuid::new_v4();
704        // Unknown task is Pending.
705        assert_eq!(backend.get_state(id).await.unwrap(), TaskState::Pending);
706
707        backend
708            .store_result(
709                id,
710                TaskResultValue::Failure {
711                    error: "boom".to_string(),
712                    traceback: None,
713                },
714            )
715            .await
716            .unwrap();
717        assert_eq!(
718            backend.get_state(id).await.unwrap(),
719            TaskState::Failed("boom".to_string())
720        );
721
722        backend
723            .store_result(
724                id,
725                TaskResultValue::Retry {
726                    attempt: 2,
727                    max_retries: 5,
728                },
729            )
730            .await
731            .unwrap();
732        assert_eq!(backend.get_state(id).await.unwrap(), TaskState::Retrying(2));
733
734        backend
735            .store_result(id, TaskResultValue::Success(json!(7)))
736            .await
737            .unwrap();
738        match backend.get_state(id).await.unwrap() {
739            TaskState::Succeeded(bytes) => {
740                assert_eq!(
741                    serde_json::from_slice::<serde_json::Value>(&bytes).unwrap(),
742                    json!(7)
743                );
744            }
745            other => panic!("expected Succeeded, got {other:?}"),
746        }
747    }
748
749    #[tokio::test]
750    async fn backend_overwrite_clears_tombstone() {
751        let backend = InMemoryResultBackend::new();
752        let id = Uuid::new_v4();
753        backend
754            .store_result(id, TaskResultValue::Success(json!(1)))
755            .await
756            .unwrap();
757        // Forget with tombstone.
758        backend
759            .forget_with_tombstone(crate::ResultTombstone::new(id))
760            .await
761            .unwrap();
762        assert!(backend.has_tombstone(id).await.unwrap());
763        assert!(!backend.has_result(id).await.unwrap());
764
765        // Re-storing clears the tombstone.
766        backend
767            .store_result(id, TaskResultValue::Success(json!(2)))
768            .await
769            .unwrap();
770        assert!(!backend.has_tombstone(id).await.unwrap());
771        assert!(backend.has_result(id).await.unwrap());
772    }
773
774    #[tokio::test]
775    async fn backend_result_existence_tri_state() {
776        use crate::result_tombstone::ResultExistence;
777        let backend = InMemoryResultBackend::new();
778        let id = Uuid::new_v4();
779        // Absent.
780        assert!(matches!(
781            backend.result_existence(id).await.unwrap(),
782            ResultExistence::Absent
783        ));
784        // Present.
785        backend
786            .store_result(id, TaskResultValue::Success(json!(1)))
787            .await
788            .unwrap();
789        assert!(matches!(
790            backend.result_existence(id).await.unwrap(),
791            ResultExistence::Present
792        ));
793        // Tombstoned.
794        backend
795            .forget_with_tombstone(crate::ResultTombstone::new(id))
796            .await
797            .unwrap();
798        assert!(matches!(
799            backend.result_existence(id).await.unwrap(),
800            ResultExistence::Tombstoned(_)
801        ));
802    }
803}