Skip to main content

commonware_storage/queue/
shared.rs

1//! Shared queue with split writer/reader handles.
2//!
3//! Provides concurrent access to a [Queue] with multiple writers and a single reader.
4//! The reader can await new items using [Reader::recv], which integrates
5//! with `select!` for multiplexing with other futures.
6//!
7//! Writers can be cloned to allow multiple tasks to enqueue items concurrently.
8
9use super::{Config, Error, Queue};
10use crate::Context;
11use commonware_codec::CodecShared;
12use commonware_utils::{
13    channel::mpsc,
14    sync::{AsyncMutex, AsyncMutexGuard},
15};
16use std::{ops::Range, sync::Arc};
17use tracing::debug;
18
19/// The shared queue cell.
20///
21/// Queue mutations take the queue by value, so shared handles keep it in an `Option`:
22/// taken out for each mutation and put back on success. If a mutation fails or its future
23/// is dropped mid-flight, the cell is left empty and every later call returns
24/// [Error::Unavailable]; reopen the queue to recover.
25type Cell<E, V> = Arc<AsyncMutex<Option<Queue<E, V>>>>;
26
27/// Take the queue out of a locked cell, or report it lost.
28fn take<E: Context, V: CodecShared>(
29    guard: &mut AsyncMutexGuard<'_, Option<Queue<E, V>>>,
30) -> Result<Queue<E, V>, Error> {
31    guard.take().ok_or(Error::Unavailable)
32}
33
34/// Borrow the queue in a locked cell, or report it lost.
35fn peek<'a, E: Context, V: CodecShared>(
36    guard: &'a AsyncMutexGuard<'_, Option<Queue<E, V>>>,
37) -> Result<&'a Queue<E, V>, Error> {
38    guard.as_ref().ok_or(Error::Unavailable)
39}
40
41/// Mutably borrow the queue in a locked cell, or report it lost.
42fn peek_mut<'a, E: Context, V: CodecShared>(
43    guard: &'a mut AsyncMutexGuard<'_, Option<Queue<E, V>>>,
44) -> Result<&'a mut Queue<E, V>, Error> {
45    guard.as_mut().ok_or(Error::Unavailable)
46}
47
48/// Writer handle for enqueueing items.
49///
50/// This handle can be cloned to allow multiple tasks to enqueue items concurrently.
51/// All clones share the same underlying queue and notification channel. Any method
52/// returns [Error::Unavailable] if an earlier mutation failed or was interrupted;
53/// reopen the queue to recover.
54pub struct Writer<E: Context, V: CodecShared> {
55    queue: Cell<E, V>,
56    notify: mpsc::Sender<()>,
57}
58
59impl<E: Context, V: CodecShared> Clone for Writer<E, V> {
60    fn clone(&self) -> Self {
61        Self {
62            queue: self.queue.clone(),
63            notify: self.notify.clone(),
64        }
65    }
66}
67
68impl<E: Context, V: CodecShared> Writer<E, V> {
69    /// Enqueue an item, returning its position. The lock is held for the
70    /// full append + commit, so no reader can see the item until it is durable.
71    ///
72    /// # Errors
73    ///
74    /// Returns an error if the underlying storage operation fails.
75    pub async fn enqueue(&self, item: V) -> Result<u64, Error> {
76        let mut guard = self.queue.lock().await;
77        let (queue, pos) = take(&mut guard)?.enqueue(item).await?;
78        *guard = Some(queue);
79        drop(guard);
80
81        // Fire-and-forget so the writer never blocks on reader wake-up.
82        // The reader always checks the queue under lock, so a missed
83        // notification never causes a missed item.
84        let _ = self.notify.try_send(());
85
86        debug!(position = pos, "writer: enqueued item");
87        Ok(pos)
88    }
89
90    /// Enqueue a batch of items with a single commit, returning positions
91    /// `[start, end)`. The lock is held for the full batch, so no reader can
92    /// see any item until the entire batch is durable.
93    ///
94    /// # Errors
95    ///
96    /// Returns an error if any append or the final commit fails.
97    pub async fn enqueue_bulk(
98        &self,
99        items: impl IntoIterator<Item = V>,
100    ) -> Result<Range<u64>, Error> {
101        let mut guard = self.queue.lock().await;
102        let mut queue = take(&mut guard)?;
103        let start = queue.size();
104        for item in items {
105            (queue, _) = queue.append(item).await?;
106        }
107        let end = queue.size();
108        if end > start {
109            queue = queue.commit().await?;
110        }
111        *guard = Some(queue);
112        drop(guard);
113
114        if start < end {
115            let _ = self.notify.try_send(());
116        }
117        debug!(start, end, "writer: enqueued bulk");
118        Ok(start..end)
119    }
120
121    /// Append an item without committing, returning its position. The item
122    /// is immediately visible to the reader but is **not durable** until
123    /// [Self::commit] or [Self::sync] is called.
124    ///
125    /// # Errors
126    ///
127    /// Returns an error if the underlying storage operation fails.
128    pub async fn append(&self, item: V) -> Result<u64, Error> {
129        let mut guard = self.queue.lock().await;
130        let (queue, pos) = take(&mut guard)?.append(item).await?;
131        *guard = Some(queue);
132        drop(guard);
133        let _ = self.notify.try_send(());
134        debug!(position = pos, "writer: appended item");
135        Ok(pos)
136    }
137
138    /// See [Queue::commit](super::Queue::commit).
139    pub async fn commit(&self) -> Result<(), Error> {
140        let mut guard = self.queue.lock().await;
141        let queue = take(&mut guard)?.commit().await?;
142        *guard = Some(queue);
143        Ok(())
144    }
145
146    /// See [Queue::sync](super::Queue::sync).
147    pub async fn sync(&self) -> Result<(), Error> {
148        let mut guard = self.queue.lock().await;
149        let queue = take(&mut guard)?.sync().await?;
150        *guard = Some(queue);
151        Ok(())
152    }
153
154    /// Returns the total number of items that have been enqueued.
155    pub async fn size(&self) -> Result<u64, Error> {
156        Ok(peek(&self.queue.lock().await)?.size())
157    }
158}
159
160/// Reader handle for dequeuing and acknowledging items.
161///
162/// There should only be one reader per shared queue. Any method returns
163/// [Error::Unavailable] if an earlier mutation failed or was interrupted; reopen the
164/// queue to recover.
165pub struct Reader<E: Context, V: CodecShared> {
166    queue: Cell<E, V>,
167    notify: mpsc::Receiver<()>,
168}
169
170impl<E: Context, V: CodecShared> Reader<E, V> {
171    /// Receive the next unacknowledged item, waiting if necessary.
172    ///
173    /// This method is designed for use with `select!`. It will:
174    /// 1. Return immediately if an unacked item is available
175    /// 2. Wait for the writer to enqueue new items if the queue is empty
176    /// 3. Return `None` if the writer is dropped (no more items will arrive)
177    ///
178    /// # Errors
179    ///
180    /// Returns an error if the underlying storage operation fails.
181    pub async fn recv(&mut self) -> Result<Option<(u64, V)>, Error> {
182        loop {
183            // Try to dequeue an item
184            if let Some(item) = self.dequeue().await? {
185                return Ok(Some(item));
186            }
187
188            // No item available, wait for notification
189            // Returns None if writer is dropped
190            if self.notify.recv().await.is_none() {
191                // Writer dropped, drain any remaining items
192                return self.dequeue().await;
193            }
194        }
195    }
196
197    /// Try to dequeue the next unacknowledged item without waiting.
198    ///
199    /// Returns `None` immediately if no unacked item is available.
200    ///
201    /// # Errors
202    ///
203    /// Returns an error if the underlying storage operation fails.
204    pub async fn try_recv(&mut self) -> Result<Option<(u64, V)>, Error> {
205        // Drain pending notification (capacity is 1, so at most 1 buffered).
206        let _ = self.notify.try_recv();
207
208        self.dequeue().await
209    }
210
211    /// Dequeue through the shared cell.
212    async fn dequeue(&self) -> Result<Option<(u64, V)>, Error> {
213        peek_mut(&mut self.queue.lock().await)?.dequeue().await
214    }
215
216    /// See [Queue::ack].
217    ///
218    /// # Errors
219    ///
220    /// Returns [super::Error::PositionOutOfRange] if the position is invalid.
221    pub async fn ack(&self, position: u64) -> Result<(), Error> {
222        peek_mut(&mut self.queue.lock().await)?.ack(position)
223    }
224
225    /// See [Queue::ack_up_to].
226    ///
227    /// # Errors
228    ///
229    /// Returns [super::Error::PositionOutOfRange] if `up_to` is invalid.
230    pub async fn ack_up_to(&self, up_to: u64) -> Result<(), Error> {
231        peek_mut(&mut self.queue.lock().await)?.ack_up_to(up_to)
232    }
233
234    /// See [Queue::ack_floor].
235    pub async fn ack_floor(&self) -> Result<u64, Error> {
236        Ok(peek(&self.queue.lock().await)?.ack_floor())
237    }
238
239    /// See [Queue::read_position].
240    pub async fn read_position(&self) -> Result<u64, Error> {
241        Ok(peek(&self.queue.lock().await)?.read_position())
242    }
243
244    /// See [Queue::is_empty].
245    pub async fn is_empty(&self) -> Result<bool, Error> {
246        Ok(peek(&self.queue.lock().await)?.is_empty())
247    }
248
249    /// See [Queue::reset].
250    pub async fn reset(&self) -> Result<(), Error> {
251        peek_mut(&mut self.queue.lock().await)?.reset();
252        Ok(())
253    }
254}
255
256/// Initialize a shared queue and split into writer and reader handles.
257///
258/// # Example
259///
260/// ```rust,ignore
261/// use commonware_macros::select;
262///
263/// let (writer, mut reader) = shared::init(context, config).await?;
264///
265/// // Writer task (clone for multiple producers)
266/// writer.enqueue(item).await?;
267///
268/// // Reader task
269/// loop {
270///     select! {
271///         result = reader.recv() => {
272///             let Some((pos, item)) = result? else { break };
273///             // Process item...
274///             reader.ack(pos).await?;
275///         }
276///         _ = shutdown => break,
277///     }
278/// }
279/// ```
280pub async fn init<E: Context, V: CodecShared>(
281    context: E,
282    cfg: Config<V::Cfg>,
283) -> Result<(Writer<E, V>, Reader<E, V>), Error> {
284    let queue = Arc::new(AsyncMutex::new(Some(Queue::init(context, cfg).await?)));
285    let (notify_tx, notify_rx) = mpsc::channel(1);
286
287    let writer = Writer {
288        queue: queue.clone(),
289        notify: notify_tx,
290    };
291
292    let reader = Reader {
293        queue,
294        notify: notify_rx,
295    };
296
297    Ok((writer, reader))
298}
299
300#[cfg(test)]
301mod tests {
302    use super::*;
303    use commonware_codec::RangeCfg;
304    use commonware_macros::{select, test_traced};
305    use commonware_runtime::{
306        BufferPooler, Clock, Runner, Spawner, Supervisor as _, buffer::paged::CacheRef,
307        deterministic,
308    };
309    use commonware_utils::{NZU16, NZU64, NZUsize};
310    use std::num::{NonZeroU16, NonZeroUsize};
311
312    const PAGE_SIZE: NonZeroU16 = NZU16!(1024);
313    const PAGE_CACHE_SIZE: NonZeroUsize = NZUsize!(10);
314
315    fn test_config(partition: &str, pooler: &impl BufferPooler) -> Config<(RangeCfg<usize>, ())> {
316        Config {
317            partition: partition.into(),
318            items_per_section: NZU64!(10),
319            compression: None,
320            codec_config: ((0..).into(), ()),
321            page_cache: CacheRef::from_pooler(pooler, PAGE_SIZE, PAGE_CACHE_SIZE),
322            write_buffer: NZUsize!(4096),
323            replay_buffer: NZUsize!(4096),
324        }
325    }
326
327    #[test_traced]
328    fn test_shared_basic() {
329        let executor = deterministic::Runner::default();
330        executor.start(|context| async move {
331            let cfg = test_config("test_shared_basic", &context);
332            let (writer, mut reader) = init(context, cfg).await.unwrap();
333
334            // Enqueue from writer
335            let pos = writer.enqueue(b"hello".to_vec()).await.unwrap();
336            assert_eq!(pos, 0);
337
338            // Receive from reader
339            let (recv_pos, item) = reader.recv().await.unwrap().unwrap();
340            assert_eq!(recv_pos, 0);
341            assert_eq!(item, b"hello".to_vec());
342
343            // Ack the item
344            reader.ack(recv_pos).await.unwrap();
345            assert!(reader.is_empty().await.unwrap());
346        });
347    }
348
349    #[test_traced]
350    fn test_shared_append_commit() {
351        let executor = deterministic::Runner::default();
352        executor.start(|context| async move {
353            let cfg = test_config("test_shared_append_commit", &context);
354            let (writer, mut reader) = init(context, cfg).await.unwrap();
355
356            // Append several items without committing
357            for i in 0..5u8 {
358                let pos = writer.append(vec![i]).await.unwrap();
359                assert_eq!(pos, i as u64);
360            }
361
362            // Reader can see them before commit
363            let (pos, item) = reader.recv().await.unwrap().unwrap();
364            assert_eq!(pos, 0);
365            assert_eq!(item, vec![0]);
366
367            // Commit to make durable
368            writer.commit().await.unwrap();
369
370            // Remaining items still readable
371            for i in 1..5 {
372                let (pos, item) = reader.recv().await.unwrap().unwrap();
373                assert_eq!(pos, i);
374                assert_eq!(item, vec![i as u8]);
375                reader.ack(pos).await.unwrap();
376            }
377
378            reader.ack(0).await.unwrap();
379            assert!(reader.is_empty().await.unwrap());
380        });
381    }
382
383    #[test_traced]
384    fn test_shared_enqueue_bulk() {
385        let executor = deterministic::Runner::default();
386        executor.start(|context| async move {
387            let cfg = test_config("test_shared_bulk", &context);
388            let (writer, mut reader) = init(context, cfg).await.unwrap();
389
390            let range = writer
391                .enqueue_bulk((0..5u8).map(|i| vec![i]))
392                .await
393                .unwrap();
394            assert_eq!(range, 0..5);
395
396            for i in 0..5 {
397                let (pos, item) = reader.recv().await.unwrap().unwrap();
398                assert_eq!(pos, i);
399                assert_eq!(item, vec![i as u8]);
400                reader.ack(pos).await.unwrap();
401            }
402            assert!(reader.is_empty().await.unwrap());
403        });
404    }
405
406    #[test_traced]
407    fn test_shared_concurrent() {
408        let executor = deterministic::Runner::default();
409        executor.start(|context| async move {
410            let cfg = test_config("test_shared_concurrent", &context);
411            let (writer, mut reader) = init(context.child("storage"), cfg).await.unwrap();
412
413            // Spawn writer task
414            let writer_handle = context.child("writer").spawn(|_ctx| async move {
415                for i in 0..10u8 {
416                    writer.enqueue(vec![i]).await.unwrap();
417                }
418                writer
419            });
420
421            // Reader receives items as they come
422            let mut received = Vec::new();
423            for _ in 0..10 {
424                let (pos, item) = reader.recv().await.unwrap().unwrap();
425                received.push((pos, item.clone()));
426                reader.ack(pos).await.unwrap();
427            }
428
429            // Verify all items received in order
430            for (i, (pos, item)) in received.iter().enumerate() {
431                assert_eq!(*pos, i as u64);
432                assert_eq!(*item, vec![i as u8]);
433            }
434
435            let _ = writer_handle.await.unwrap();
436        });
437    }
438
439    #[test_traced]
440    fn test_shared_select() {
441        let executor = deterministic::Runner::default();
442        executor.start(|context| async move {
443            let cfg = test_config("test_shared_select", &context);
444            let (writer, mut reader) = init(context.child("storage"), cfg).await.unwrap();
445
446            // Enqueue an item
447            writer.enqueue(b"test".to_vec()).await.unwrap();
448
449            // Use select to receive with timeout
450            let result = select! {
451                item = reader.recv() => item,
452                _ = context.sleep(std::time::Duration::from_secs(1)) => {
453                    panic!("timeout")
454                },
455            };
456
457            let (pos, item) = result.unwrap().unwrap();
458            assert_eq!(pos, 0);
459            assert_eq!(item, b"test".to_vec());
460
461            reader.ack(pos).await.unwrap();
462        });
463    }
464
465    #[test_traced]
466    fn test_shared_writer_dropped() {
467        let executor = deterministic::Runner::default();
468        executor.start(|context| async move {
469            let cfg = test_config("test_shared_writer_dropped", &context);
470            let (writer, mut reader) = init(context.child("storage"), cfg).await.unwrap();
471
472            // Enqueue items then drop writer
473            writer.enqueue(b"item1".to_vec()).await.unwrap();
474            writer.enqueue(b"item2".to_vec()).await.unwrap();
475
476            // Get the queue before dropping writer
477            let queue = writer.queue.clone();
478            drop(writer);
479
480            // Reader should still get existing items
481            let (pos1, _) = reader.recv().await.unwrap().unwrap();
482            reader.ack(pos1).await.unwrap();
483
484            let (pos2, _) = reader.recv().await.unwrap().unwrap();
485            reader.ack(pos2).await.unwrap();
486
487            // Next recv should return None (writer dropped, queue empty)
488            let result = reader.recv().await.unwrap();
489            assert!(result.is_none());
490
491            drop(reader);
492            let _ = Arc::try_unwrap(queue)
493                .unwrap_or_else(|_| panic!("queue should have a single reference"))
494                .into_inner();
495        });
496    }
497
498    #[test_traced]
499    fn test_shared_try_recv() {
500        let executor = deterministic::Runner::default();
501        executor.start(|context| async move {
502            let cfg = test_config("test_shared_try_recv", &context);
503            let (writer, mut reader) = init(context, cfg).await.unwrap();
504
505            // try_recv on empty queue returns None
506            let result = reader.try_recv().await.unwrap();
507            assert!(result.is_none());
508
509            // Enqueue and try_recv
510            writer.enqueue(b"item".to_vec()).await.unwrap();
511            let (pos, item) = reader.try_recv().await.unwrap().unwrap();
512            assert_eq!(pos, 0);
513            assert_eq!(item, b"item".to_vec());
514
515            reader.ack(pos).await.unwrap();
516        });
517    }
518
519    #[test_traced]
520    fn test_shared_multiple_writers() {
521        let executor = deterministic::Runner::default();
522        executor.start(|context| async move {
523            let cfg = test_config("test_shared_multi_writer", &context);
524            let (writer, mut reader) = init(context.child("storage"), cfg).await.unwrap();
525
526            // Clone writer for second task
527            let writer2 = writer.clone();
528
529            // Spawn two writer tasks
530            let handle1 =
531                context
532                    .child("writer")
533                    .with_attribute("index", 1)
534                    .spawn(|_ctx| async move {
535                        for i in 0..5u8 {
536                            writer.enqueue(vec![i]).await.unwrap();
537                        }
538                        writer
539                    });
540
541            let handle2 =
542                context
543                    .child("writer")
544                    .with_attribute("index", 2)
545                    .spawn(|_ctx| async move {
546                        for i in 5..10u8 {
547                            writer2.enqueue(vec![i]).await.unwrap();
548                        }
549                    });
550
551            // Reader receives all 10 items
552            let mut received = Vec::new();
553            for _ in 0..10 {
554                let (pos, item) = reader.recv().await.unwrap().unwrap();
555                received.push(item[0]);
556                reader.ack(pos).await.unwrap();
557            }
558
559            // All items should be received (order may vary due to concurrent writes)
560            received.sort();
561            assert_eq!(received, (0..10u8).collect::<Vec<_>>());
562
563            let _ = handle1.await.unwrap();
564            handle2.await.unwrap();
565        });
566    }
567}