Skip to main content

nodedb_lite/sync/outbound/
queue.rs

1//! Generic primitives shared by every per-engine outbound sync queue.
2//!
3//! Each `*Outbound` (columnar, vector, fts, spatial, timeseries) shares the
4//! same shape: a mutex-protected `Vec<Pending…>` plus a monotonic batch-ID
5//! counter for ACK correlation. Before this module was extracted that pattern
6//! was copy-pasted across five files (~1.1k lines of near-identical
7//! drain/ack/requeue plumbing). It now lives here exactly once.
8//!
9//! Engine-specific code only writes:
10//!   * the `Pending…` struct (a row payload + a `batch_id` field)
11//!   * the public-facing wrapper that calls `enqueue` / `drain` / `retain` /
12//!     `requeue` and exposes engine-specific `acknowledge_*` semantics
13//!
14//! The mutex guards are deliberately recovered with `let Ok(mut g) = lock`:
15//! a poisoned outbound queue means another thread already crashed mid-sync,
16//! and silently dropping further sync work is preferable to propagating the
17//! poison and tearing down the whole runtime — the writes are already durable
18//! in the local engine.
19
20use std::sync::Mutex;
21use std::sync::atomic::{AtomicU64, Ordering};
22
23/// Monotonic batch-ID generator shared between sibling queues (e.g. the
24/// insert and delete queues of a single engine) so every in-flight ACK ID is
25/// globally unique inside one outbound.
26#[derive(Debug)]
27pub struct BatchIdGen {
28    next: AtomicU64,
29}
30
31impl BatchIdGen {
32    pub const fn new() -> Self {
33        Self {
34            next: AtomicU64::new(1),
35        }
36    }
37
38    /// Allocate the next batch ID. Always non-zero and strictly increasing
39    /// within the lifetime of this generator.
40    pub fn next(&self) -> u64 {
41        self.next.fetch_add(1, Ordering::Relaxed)
42    }
43}
44
45impl Default for BatchIdGen {
46    fn default() -> Self {
47        Self::new()
48    }
49}
50
51/// Return the current value of `counter` and advance it by one.
52///
53/// Returns `None` when the counter is at `u64::MAX`; the counter is left
54/// unchanged.
55pub(crate) fn take_next(counter: &AtomicU64) -> Option<u64> {
56    let mut current = counter.load(Ordering::Relaxed);
57    loop {
58        let advanced = current.checked_add(1)?;
59        match counter.compare_exchange_weak(current, advanced, Ordering::Relaxed, Ordering::Relaxed)
60        {
61            Ok(previous) => return Some(previous),
62            Err(observed) => current = observed,
63        }
64    }
65}
66
67/// Mutex-wrapped pending queue. `T` is the engine-specific pending entry
68/// (e.g. `PendingVectorInsert`).
69#[derive(Debug)]
70pub struct PendingQueue<T> {
71    items: Mutex<Vec<T>>,
72}
73
74impl<T> Default for PendingQueue<T> {
75    fn default() -> Self {
76        Self::new()
77    }
78}
79
80impl<T> PendingQueue<T> {
81    pub const fn new() -> Self {
82        Self {
83            items: Mutex::new(Vec::new()),
84        }
85    }
86
87    /// Append a new pending entry to the tail of the queue.
88    pub fn push(&self, item: T) {
89        if let Ok(mut g) = self.items.lock() {
90            g.push(item);
91        }
92    }
93
94    /// Take every pending entry and reset the queue. Callers retain the
95    /// returned vec until they receive ACKs.
96    pub fn drain(&self) -> Vec<T> {
97        match self.items.lock() {
98            Ok(mut g) => std::mem::take(&mut *g),
99            Err(_) => Vec::new(),
100        }
101    }
102
103    /// Re-queue an entry at the head so it is retried before fresh writes on
104    /// the next drain cycle. Used on transport reject.
105    pub fn requeue(&self, item: T) {
106        if let Ok(mut g) = self.items.lock() {
107            g.insert(0, item);
108        }
109    }
110
111    /// Drop entries matching `predicate` (used by ACK paths that key off
112    /// `batch_id` or collection name).
113    pub fn retain<F>(&self, predicate: F)
114    where
115        F: FnMut(&T) -> bool,
116    {
117        if let Ok(mut g) = self.items.lock() {
118            g.retain(predicate);
119        }
120    }
121
122    /// Run `f` on the first entry that matches `predicate`, returning its
123    /// result. Used by engines (columnar, timeseries) that coalesce rows for
124    /// the same collection into one open batch.
125    ///
126    /// Returns `None` if no entry matches (or the lock is poisoned).
127    pub fn with_first_mut<P, F, R>(&self, mut predicate: P, f: F) -> Option<R>
128    where
129        P: FnMut(&T) -> bool,
130        F: FnOnce(&mut T) -> R,
131    {
132        let mut g = self.items.lock().ok()?;
133        let entry = g.iter_mut().find(|t| predicate(t))?;
134        Some(f(entry))
135    }
136
137    /// Number of pending entries (best-effort; returns 0 on lock poison).
138    pub fn len(&self) -> usize {
139        self.items.lock().map(|g| g.len()).unwrap_or(0)
140    }
141
142    pub fn is_empty(&self) -> bool {
143        self.len() == 0
144    }
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150
151    #[test]
152    fn batch_id_gen_is_monotonic_and_nonzero() {
153        let g = BatchIdGen::new();
154        let a = g.next();
155        let b = g.next();
156        let c = g.next();
157        assert!(a > 0 && b > a && c > b);
158    }
159
160    #[test]
161    fn take_next_stops_at_the_counter_limit() {
162        let counter = AtomicU64::new(u64::MAX - 1);
163        assert_eq!(take_next(&counter), Some(u64::MAX - 1));
164        assert_eq!(take_next(&counter), None);
165        assert_eq!(counter.load(Ordering::Relaxed), u64::MAX);
166    }
167
168    #[test]
169    fn push_drain_round_trip() {
170        let q = PendingQueue::<u32>::new();
171        q.push(1);
172        q.push(2);
173        let out = q.drain();
174        assert_eq!(out, vec![1, 2]);
175        assert!(q.drain().is_empty());
176    }
177
178    #[test]
179    fn requeue_inserts_at_head() {
180        let q = PendingQueue::<u32>::new();
181        q.push(1);
182        q.requeue(99);
183        assert_eq!(q.drain(), vec![99, 1]);
184    }
185
186    #[test]
187    fn retain_removes_matches() {
188        let q = PendingQueue::<u32>::new();
189        for i in 0..5 {
190            q.push(i);
191        }
192        q.retain(|x| *x % 2 == 0);
193        assert_eq!(q.drain(), vec![0, 2, 4]);
194    }
195
196    #[test]
197    fn with_first_mut_targets_match_only() {
198        let q = PendingQueue::<(u32, u32)>::new();
199        q.push((1, 10));
200        q.push((2, 20));
201        let touched = q.with_first_mut(|x| x.0 == 2, |x| x.1 += 5);
202        assert_eq!(touched, Some(()));
203        assert_eq!(q.drain(), vec![(1, 10), (2, 25)]);
204    }
205}