1use std::future::Future;
30use std::sync::Arc;
31
32use nodedb_array::error::{ArrayError, ArrayResult};
33use nodedb_array::sync::hlc::Hlc;
34use nodedb_array::sync::op::ArrayOp;
35use nodedb_array::sync::op_codec;
36use nodedb_array::sync::op_log::{OpIter, OpLog};
37use nodedb_types::Namespace;
38
39use crate::error::LiteError;
40use crate::storage::engine::{StorageEngine, WriteOp};
41
42pub struct KvOpLogStore<S: StorageEngine> {
49 storage: Arc<S>,
50}
51
52impl<S: StorageEngine> KvOpLogStore<S> {
53 pub fn new(storage: Arc<S>) -> Self {
55 Self { storage }
56 }
57}
58
59fn make_key(array: &str, hlc: Hlc) -> ArrayResult<Vec<u8>> {
66 let name = array.as_bytes();
67 if name.is_empty() {
68 return Err(ArrayError::SegmentCorruption {
69 detail: "op_log: array name must not be empty".into(),
70 });
71 }
72 if name.len() > 255 {
73 return Err(ArrayError::SegmentCorruption {
74 detail: "op_log: array name exceeds 255 bytes".into(),
75 });
76 }
77 let mut key = Vec::with_capacity(1 + name.len() + 18);
78 key.push(name.len() as u8);
79 key.extend_from_slice(name);
80 key.extend_from_slice(&hlc.to_bytes());
81 Ok(key)
82}
83
84fn make_array_prefix(array: &str) -> ArrayResult<Vec<u8>> {
89 let name = array.as_bytes();
90 if name.is_empty() {
91 return Err(ArrayError::SegmentCorruption {
92 detail: "op_log: array name must not be empty".into(),
93 });
94 }
95 if name.len() > 255 {
96 return Err(ArrayError::SegmentCorruption {
97 detail: "op_log: array name exceeds 255 bytes".into(),
98 });
99 }
100 let mut prefix = Vec::with_capacity(1 + name.len());
101 prefix.push(name.len() as u8);
102 prefix.extend_from_slice(name);
103 Ok(prefix)
104}
105
106fn next_prefix(prefix: &[u8]) -> Option<Vec<u8>> {
112 let mut upper = prefix.to_vec();
113 for byte in upper.iter_mut().rev() {
114 if *byte < 0xFF {
115 *byte += 1;
116 return Some(upper);
117 }
118 *byte = 0x00;
119 }
120 None
121}
122
123fn split_key(key: &[u8]) -> Option<(String, Hlc)> {
128 if key.is_empty() {
129 return None;
130 }
131 let name_len = key[0] as usize;
132 let name_end = 1 + name_len;
133 if key.len() < name_end + 18 {
134 return None;
135 }
136 let name = std::str::from_utf8(&key[1..name_end]).ok()?;
137 let hlc_bytes: &[u8; 18] = key[name_end..name_end + 18].try_into().ok()?;
138 Some((name.to_owned(), Hlc::from_bytes(hlc_bytes)))
139}
140
141fn lite_err_to_array(e: LiteError) -> ArrayError {
143 ArrayError::SegmentCorruption {
144 detail: format!("op_log: {e}"),
145 }
146}
147
148fn block<F, T>(f: F) -> T
159where
160 F: Future<Output = T>,
161{
162 tokio::task::block_in_place(|| tokio::runtime::Handle::current().block_on(f))
163}
164
165impl<S: StorageEngine> OpLog for KvOpLogStore<S> {
166 fn append(&self, op: &ArrayOp) -> ArrayResult<()> {
171 let key = make_key(&op.header.array, op.header.hlc)?;
172 let value = op_codec::encode_op(op)?;
173 block(self.storage.put(Namespace::ArrayOpLog, &key, &value)).map_err(lite_err_to_array)
174 }
175
176 fn scan_from<'a>(&'a self, from: Hlc) -> ArrayResult<OpIter<'a>> {
182 let pairs = block(
183 self.storage
184 .scan_range(Namespace::ArrayOpLog, &[], usize::MAX),
185 )
186 .map_err(lite_err_to_array)?;
187
188 let ops: Vec<ArrayResult<ArrayOp>> = pairs
189 .into_iter()
190 .filter_map(|(key, value)| {
191 let (_, hlc) = split_key(&key)?;
192 if hlc < from {
193 return None;
194 }
195 Some(op_codec::decode_op(&value))
196 })
197 .collect();
198
199 Ok(Box::new(ops.into_iter()))
200 }
201
202 fn scan_range<'a>(&'a self, array: &str, from: Hlc, to: Hlc) -> ArrayResult<OpIter<'a>> {
208 let start = make_key(array, from)?;
209 if from > to {
210 return Ok(Box::new(std::iter::empty()));
211 }
212 let end = next_prefix(&make_key(array, to)?);
215
216 let pairs = block(self.storage.scan_range_bounded(
217 Namespace::ArrayOpLog,
218 Some(&start),
219 end.as_deref(),
220 None,
221 ))
222 .map_err(lite_err_to_array)?;
223
224 let prefix = make_array_prefix(array)?;
225 let ops: Vec<ArrayResult<ArrayOp>> = pairs
226 .into_iter()
227 .take_while(|(key, _)| key.starts_with(&prefix))
228 .filter_map(|(key, value)| {
229 let (_, hlc) = split_key(&key)?;
230 if hlc < from || hlc > to {
231 return None;
232 }
233 Some(op_codec::decode_op(&value))
234 })
235 .collect();
236
237 Ok(Box::new(ops.into_iter()))
238 }
239
240 fn len(&self) -> ArrayResult<u64> {
242 block(self.storage.count(Namespace::ArrayOpLog)).map_err(lite_err_to_array)
243 }
244
245 fn drop_below(&self, hlc: Hlc) -> ArrayResult<u64> {
247 let pairs = block(
248 self.storage
249 .scan_range(Namespace::ArrayOpLog, &[], usize::MAX),
250 )
251 .map_err(lite_err_to_array)?;
252
253 let to_delete: Vec<WriteOp> = pairs
254 .into_iter()
255 .filter_map(|(key, _)| {
256 let (_, entry_hlc) = split_key(&key)?;
257 if entry_hlc < hlc {
258 Some(WriteOp::Delete {
259 ns: Namespace::ArrayOpLog,
260 key,
261 })
262 } else {
263 None
264 }
265 })
266 .collect();
267
268 let count = to_delete.len() as u64;
269 if !to_delete.is_empty() {
270 block(self.storage.batch_write(&to_delete)).map_err(lite_err_to_array)?;
271 }
272 Ok(count)
273 }
274}
275
276#[cfg(test)]
279mod tests {
280 use super::*;
281 use nodedb_array::sync::op::{ArrayOpHeader, ArrayOpKind};
282 use nodedb_array::sync::replica_id::ReplicaId;
283 use nodedb_array::types::cell_value::value::CellValue;
284 use nodedb_array::types::coord::value::CoordValue;
285
286 use crate::storage::pagedb_storage::{PagedbStorageDefault, PagedbStorageMem};
287
288 fn replica() -> ReplicaId {
289 ReplicaId::new(1)
290 }
291
292 fn hlc(ms: u64, logical: u16) -> Hlc {
293 Hlc::new(ms, logical, replica()).unwrap()
294 }
295
296 fn make_op(array: &str, ms: u64) -> ArrayOp {
297 ArrayOp {
298 header: ArrayOpHeader {
299 array: array.into(),
300 hlc: hlc(ms, 0),
301 schema_hlc: hlc(1, 0),
302 valid_from_ms: 0,
303 valid_until_ms: -1,
304 system_from_ms: ms as i64,
305 },
306 kind: ArrayOpKind::Put,
307 coord: vec![CoordValue::Int64(ms as i64)],
308 attrs: Some(vec![CellValue::Null]),
309 }
310 }
311
312 async fn make_log() -> KvOpLogStore<PagedbStorageMem> {
313 let storage = Arc::new(PagedbStorageMem::open_in_memory().await.unwrap());
314 KvOpLogStore::new(storage)
315 }
316
317 #[tokio::test(flavor = "multi_thread")]
321 async fn append_then_scan_returns_op() {
322 let log = make_log().await;
323 log.append(&make_op("arr", 10)).unwrap();
324 log.append(&make_op("arr", 20)).unwrap();
325
326 let ops: Vec<_> = log
327 .scan_from(Hlc::ZERO)
328 .unwrap()
329 .map(|r| r.unwrap())
330 .collect();
331 assert_eq!(ops.len(), 2);
332 }
333
334 #[tokio::test(flavor = "multi_thread")]
335 async fn scan_from_filters_below() {
336 let log = make_log().await;
337 log.append(&make_op("arr", 10)).unwrap();
338 log.append(&make_op("arr", 20)).unwrap();
339 log.append(&make_op("arr", 30)).unwrap();
340
341 let ops: Vec<_> = log
342 .scan_from(hlc(20, 0))
343 .unwrap()
344 .map(|r| r.unwrap())
345 .collect();
346 assert_eq!(ops.len(), 2);
347 assert!(ops.iter().all(|op| op.header.hlc.physical_ms >= 20));
348 }
349
350 #[tokio::test(flavor = "multi_thread")]
351 async fn scan_range_filters_array() {
352 let log = make_log().await;
353 log.append(&make_op("a", 10)).unwrap();
354 log.append(&make_op("b", 20)).unwrap();
355 log.append(&make_op("a", 30)).unwrap();
356
357 let ops: Vec<_> = log
358 .scan_range("a", Hlc::ZERO, hlc(u64::MAX >> 16, 0))
359 .unwrap()
360 .map(|r| r.unwrap())
361 .collect();
362 assert_eq!(ops.len(), 2);
363 assert!(ops.iter().all(|op| op.header.array == "a"));
364 }
365
366 #[tokio::test(flavor = "multi_thread")]
367 async fn scan_range_filters_inclusive_bounds() {
368 let log = make_log().await;
369 log.append(&make_op("arr", 10)).unwrap();
370 log.append(&make_op("arr", 20)).unwrap();
371 log.append(&make_op("arr", 30)).unwrap();
372
373 let ops: Vec<_> = log
375 .scan_range("arr", hlc(10, 0), hlc(20, 0))
376 .unwrap()
377 .map(|r| r.unwrap())
378 .collect();
379 assert_eq!(ops.len(), 2);
380 let ms: Vec<u64> = ops.iter().map(|op| op.header.hlc.physical_ms).collect();
381 assert!(ms.contains(&10));
382 assert!(ms.contains(&20));
383 assert!(!ms.contains(&30));
384 }
385
386 #[tokio::test(flavor = "multi_thread")]
387 async fn drop_below_drops_correctly() {
388 let log = make_log().await;
389 log.append(&make_op("arr", 10)).unwrap();
390 log.append(&make_op("arr", 20)).unwrap();
391 log.append(&make_op("arr", 30)).unwrap();
392
393 let dropped = log.drop_below(hlc(20, 0)).unwrap();
395 assert_eq!(dropped, 1);
396 assert_eq!(log.len().unwrap(), 2);
397
398 let ops: Vec<_> = log
399 .scan_from(Hlc::ZERO)
400 .unwrap()
401 .map(|r| r.unwrap())
402 .collect();
403 assert!(ops.iter().all(|op| op.header.hlc.physical_ms >= 20));
404 }
405
406 #[tokio::test(flavor = "multi_thread")]
407 async fn len_counts_correctly() {
408 let log = make_log().await;
409 assert_eq!(log.len().unwrap(), 0);
410 log.append(&make_op("x", 1)).unwrap();
411 log.append(&make_op("y", 2)).unwrap();
412 assert_eq!(log.len().unwrap(), 2);
413 }
414
415 #[tokio::test(flavor = "multi_thread")]
416 async fn idempotent_append() {
417 let log = make_log().await;
418 log.append(&make_op("arr", 10)).unwrap();
419 log.append(&make_op("arr", 10)).unwrap();
420 assert_eq!(log.len().unwrap(), 1);
421 }
422
423 #[test]
424 fn array_name_too_long_errors() {
425 let long_name = "a".repeat(256);
427 let name_bytes = long_name.as_bytes();
428 assert!(name_bytes.len() > 255);
429 let err = make_key(&long_name, Hlc::ZERO).unwrap_err();
430 assert!(
431 matches!(err, ArrayError::SegmentCorruption { ref detail } if detail.contains("255")),
432 "unexpected error: {err:?}"
433 );
434 }
435
436 #[tokio::test(flavor = "multi_thread")]
437 async fn decode_corruption_propagates() {
438 let storage = Arc::new(PagedbStorageMem::open_in_memory().await.unwrap());
439 let log = KvOpLogStore::new(Arc::clone(&storage));
440
441 let valid_key = make_key("arr", hlc(99, 0)).unwrap();
443 storage
444 .put(Namespace::ArrayOpLog, &valid_key, b"\xff\xfe garbage")
445 .await
446 .unwrap();
447
448 let results: Vec<_> = log.scan_from(Hlc::ZERO).unwrap().collect();
449 assert_eq!(results.len(), 1);
450 assert!(results[0].is_err(), "expected decode error, got Ok");
451 }
452
453 #[tokio::test(flavor = "multi_thread")]
454 async fn survives_storage_restart() {
455 let dir = tempfile::tempdir().unwrap();
456 let path = dir.path().join("op_log_test.pagedb");
457
458 {
459 let storage = Arc::new(
460 PagedbStorageDefault::open(
461 &path,
462 crate::storage::encryption::Encryption::Plaintext,
463 )
464 .await
465 .unwrap(),
466 );
467 let log = KvOpLogStore::new(Arc::clone(&storage));
468 log.append(&make_op("arr", 10)).unwrap();
469 log.append(&make_op("arr", 20)).unwrap();
470 }
471
472 {
474 let storage = Arc::new(
475 PagedbStorageDefault::open(
476 &path,
477 crate::storage::encryption::Encryption::Plaintext,
478 )
479 .await
480 .unwrap(),
481 );
482 let log = KvOpLogStore::new(storage);
483 assert_eq!(log.len().unwrap(), 2);
484 let ops: Vec<_> = log
485 .scan_from(Hlc::ZERO)
486 .unwrap()
487 .map(|r| r.unwrap())
488 .collect();
489 assert_eq!(ops.len(), 2);
490 }
491 }
492
493 #[test]
494 fn next_prefix_increments_last_byte() {
495 assert_eq!(next_prefix(&[0x01, 0x61]), Some(vec![0x01, 0x62]));
496 assert_eq!(next_prefix(&[0x01, 0xFF]), Some(vec![0x02, 0x00]));
497 assert_eq!(next_prefix(&[0xFF, 0xFF]), None);
498 }
499}