nodedb_lite/sync/outbound/
durable_queue.rs1use std::sync::Arc;
23use std::sync::atomic::AtomicU64;
24
25use nodedb_types::Namespace;
26
27use crate::error::LiteError;
28use crate::storage::engine::{StorageEngine, WriteOp};
29
30pub struct DurableOutboundQueue<S: StorageEngine> {
32 storage: Arc<S>,
33 namespace: Namespace,
34 cap: usize,
35 next_id: AtomicU64,
40}
41
42impl<S: StorageEngine> DurableOutboundQueue<S> {
43 pub const DEFAULT_CAP: usize = 100_000;
45
46 pub async fn open(storage: Arc<S>, namespace: Namespace) -> Result<Self, LiteError> {
49 Self::open_with_cap(storage, namespace, Self::DEFAULT_CAP).await
50 }
51
52 pub async fn open_with_cap(
54 storage: Arc<S>,
55 namespace: Namespace,
56 cap: usize,
57 ) -> Result<Self, LiteError> {
58 let next_id = Self::recover_next_id(&storage, namespace).await?;
60 Ok(Self {
61 storage,
62 namespace,
63 cap,
64 next_id: AtomicU64::new(next_id),
65 })
66 }
67
68 async fn recover_next_id(storage: &Arc<S>, namespace: Namespace) -> Result<u64, LiteError> {
70 let pairs = storage.scan_range(namespace, &[], usize::MAX).await?;
74 let max = pairs.last().and_then(|(key, _)| {
75 if key.len() == 8 {
76 Some(u64::from_be_bytes(key[..8].try_into().ok()?))
77 } else {
78 None
79 }
80 });
81 max.map_or(Ok(1), |id| id.checked_add(1).ok_or_else(Self::exhausted))
82 }
83
84 pub async fn enqueue(&self, payload: &[u8]) -> Result<(), LiteError> {
88 self.ensure_room(1).await?;
89 let key = self.reserve_key()?;
90 self.storage.put(self.namespace, &key, payload).await
91 }
92
93 pub(crate) fn reserve_key(&self) -> Result<[u8; 8], LiteError> {
95 super::queue::take_next(&self.next_id)
96 .map(u64::to_be_bytes)
97 .ok_or_else(Self::exhausted)
98 }
99
100 fn exhausted() -> LiteError {
101 LiteError::Backpressure { detail: "outbound key counter exhausted: drain and acknowledge pending entries before reopening the queue".into() }
102 }
103
104 pub(crate) async fn persist_reserved(
108 &self,
109 key: &[u8; 8],
110 payload: &[u8],
111 ) -> Result<(), LiteError> {
112 if self.storage.get(self.namespace, key).await?.is_some() {
113 return Ok(());
114 }
115 self.ensure_room(1).await?;
116 self.storage.put(self.namespace, key, payload).await
117 }
118
119 pub async fn ensure_room(&self, entries: usize) -> Result<(), LiteError> {
125 let current = self.len().await?;
126 if current.saturating_add(entries as u64) > self.cap as u64 {
127 return Err(LiteError::Backpressure {
128 detail: format!(
129 "outbound pending queue full ({current} pending, {entries} more, cap {}); \
130 writes paused until Origin sync drains the queue",
131 self.cap
132 ),
133 });
134 }
135 Ok(())
136 }
137
138 pub async fn drain_batch(&self, limit: usize) -> Result<Vec<(Vec<u8>, Vec<u8>)>, LiteError> {
143 self.storage.scan_range(self.namespace, &[], limit).await
144 }
145
146 pub async fn ack_keys(&self, keys: &[Vec<u8>]) -> Result<(), LiteError> {
152 if keys.is_empty() {
153 return Ok(());
154 }
155 let ops: Vec<WriteOp> = keys
156 .iter()
157 .map(|key| WriteOp::Delete {
158 ns: self.namespace,
159 key: key.clone(),
160 })
161 .collect();
162 self.storage.batch_write(&ops).await
163 }
164
165 pub async fn update_entry(&self, key: &[u8], payload: &[u8]) -> Result<(), LiteError> {
170 self.storage.put(self.namespace, key, payload).await
171 }
172
173 pub async fn len(&self) -> Result<u64, LiteError> {
175 self.storage.count(self.namespace).await
176 }
177
178 pub async fn is_empty(&self) -> Result<bool, LiteError> {
180 self.len().await.map(|n| n == 0)
181 }
182}
183
184#[cfg(test)]
187mod tests {
188 use super::*;
189 use crate::storage::pagedb_storage::PagedbStorageMem;
190 use std::sync::atomic::Ordering;
191
192 async fn make_queue() -> DurableOutboundQueue<PagedbStorageMem> {
193 let storage = Arc::new(PagedbStorageMem::open_in_memory().await.unwrap());
194 DurableOutboundQueue::open(storage, Namespace::ColumnarPending)
195 .await
196 .unwrap()
197 }
198
199 #[tokio::test]
200 async fn enqueue_persists_and_drain_fifo() {
201 let q = make_queue().await;
202 q.enqueue(b"payload-a").await.unwrap();
203 q.enqueue(b"payload-b").await.unwrap();
204 q.enqueue(b"payload-c").await.unwrap();
205
206 let pairs = q.drain_batch(usize::MAX).await.unwrap();
207 assert_eq!(pairs.len(), 3);
208 let payloads: Vec<&[u8]> = pairs.iter().map(|(_, v)| v.as_slice()).collect();
209 assert_eq!(payloads, vec![b"payload-a", b"payload-b", b"payload-c"]);
210 }
211
212 #[tokio::test]
213 async fn drain_batch_respects_limit() {
214 let q = make_queue().await;
215 q.enqueue(b"a").await.unwrap();
216 q.enqueue(b"b").await.unwrap();
217 q.enqueue(b"c").await.unwrap();
218
219 let pairs = q.drain_batch(2).await.unwrap();
220 assert_eq!(pairs.len(), 2);
221 }
222
223 #[tokio::test]
224 async fn ack_keys_deletes_entries() {
225 let q = make_queue().await;
226 q.enqueue(b"x").await.unwrap();
227 q.enqueue(b"y").await.unwrap();
228 q.enqueue(b"z").await.unwrap();
229
230 let pairs = q.drain_batch(2).await.unwrap();
231 assert_eq!(pairs.len(), 2);
232 let keys: Vec<Vec<u8>> = pairs.into_iter().map(|(k, _)| k).collect();
233 q.ack_keys(&keys).await.unwrap();
234
235 assert_eq!(q.len().await.unwrap(), 1);
236 let remaining = q.drain_batch(usize::MAX).await.unwrap();
237 assert_eq!(remaining[0].1, b"z");
238 }
239
240 #[tokio::test]
241 async fn len_and_is_empty() {
242 let q = make_queue().await;
243 assert!(q.is_empty().await.unwrap());
244 q.enqueue(b"data").await.unwrap();
245 assert_eq!(q.len().await.unwrap(), 1);
246 assert!(!q.is_empty().await.unwrap());
247 }
248
249 #[tokio::test]
250 async fn cap_returns_backpressure() {
251 let storage = Arc::new(PagedbStorageMem::open_in_memory().await.unwrap());
252 let q = DurableOutboundQueue::open_with_cap(storage, Namespace::ColumnarPending, 2)
253 .await
254 .unwrap();
255
256 q.enqueue(b"a").await.unwrap();
257 q.enqueue(b"b").await.unwrap();
258 let err = q.enqueue(b"c").await.unwrap_err();
259 assert!(
260 matches!(err, LiteError::Backpressure { .. }),
261 "expected Backpressure, got: {err:?}"
262 );
263 }
264
265 #[tokio::test]
266 async fn update_entry_persists_and_re_drain_reuses_payload() {
267 let q = make_queue().await;
269 q.enqueue(b"seq=0").await.unwrap();
270
271 let pairs = q.drain_batch(usize::MAX).await.unwrap();
272 assert_eq!(pairs.len(), 1);
273 let key = pairs[0].0.clone();
274 assert_eq!(pairs[0].1, b"seq=0");
275
276 q.update_entry(&key, b"seq=7").await.unwrap();
278
279 let re = q.drain_batch(usize::MAX).await.unwrap();
282 assert_eq!(re.len(), 1, "update_entry must not create a new entry");
283 assert_eq!(re[0].0, key, "key is stable across update");
284 assert_eq!(
285 re[0].1, b"seq=7",
286 "re-drain returns the persisted seq payload"
287 );
288 assert_eq!(q.len().await.unwrap(), 1);
289 }
290
291 #[tokio::test]
292 async fn survives_reload() {
293 let storage = Arc::new(PagedbStorageMem::open_in_memory().await.unwrap());
294 {
295 let q = DurableOutboundQueue::open(Arc::clone(&storage), Namespace::ColumnarPending)
296 .await
297 .unwrap();
298 q.enqueue(b"first").await.unwrap();
299 q.enqueue(b"second").await.unwrap();
300 }
301 let q = DurableOutboundQueue::open(Arc::clone(&storage), Namespace::ColumnarPending)
303 .await
304 .unwrap();
305 assert_eq!(q.len().await.unwrap(), 2);
306 q.enqueue(b"third").await.unwrap();
308 assert_eq!(q.len().await.unwrap(), 3);
309
310 let pairs = q.drain_batch(usize::MAX).await.unwrap();
311 let payloads: Vec<&[u8]> = pairs.iter().map(|(_, v)| v.as_slice()).collect();
312 assert_eq!(
313 payloads,
314 vec![
315 b"first".as_slice(),
316 b"second".as_slice(),
317 b"third".as_slice()
318 ]
319 );
320 }
321 #[tokio::test]
322 async fn reserved_retry_preserves_payload_at_capacity() {
323 let storage = Arc::new(PagedbStorageMem::open_in_memory().await.unwrap());
324 let queue = DurableOutboundQueue::open_with_cap(storage, Namespace::FtsIndexPending, 1)
325 .await
326 .unwrap();
327 let key = queue.reserve_key().unwrap();
328 queue.persist_reserved(&key, b"seq=7").await.unwrap();
329 queue.persist_reserved(&key, b"seq=0").await.unwrap();
330 assert_eq!(
331 queue.drain_batch(10).await.unwrap(),
332 vec![(key.to_vec(), b"seq=7".to_vec())]
333 );
334 }
335
336 #[tokio::test]
337 async fn exhausted_counter_never_wraps_or_overwrites_existing_keys() {
338 let queue = make_queue().await;
339 queue.next_id.store(u64::MAX - 1, Ordering::Relaxed);
340 assert_eq!(queue.reserve_key().unwrap(), (u64::MAX - 1).to_be_bytes());
341 assert!(matches!(
342 queue.reserve_key(),
343 Err(LiteError::Backpressure { .. })
344 ));
345 assert!(matches!(
346 queue.reserve_key(),
347 Err(LiteError::Backpressure { .. })
348 ));
349 let storage = Arc::new(PagedbStorageMem::open_in_memory().await.unwrap());
350 storage
351 .put(Namespace::ColumnarPending, &u64::MAX.to_be_bytes(), b"last")
352 .await
353 .unwrap();
354 assert!(matches!(
355 DurableOutboundQueue::open(storage, Namespace::ColumnarPending).await,
356 Err(LiteError::Backpressure { .. })
357 ));
358 }
359}