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