1use std::collections::VecDeque;
7use std::sync::atomic::{AtomicU64, Ordering};
8use std::sync::Mutex;
9
10use serde::{Deserialize, Serialize};
11use thiserror::Error;
12
13pub fn fnv1a_32(data: &[u8]) -> u32 {
19 const OFFSET_BASIS: u32 = 2_166_136_261;
20 const PRIME: u32 = 16_777_619;
21
22 let mut hash = OFFSET_BASIS;
23 for &byte in data {
24 hash ^= u32::from(byte);
25 hash = hash.wrapping_mul(PRIME);
26 }
27 hash
28}
29
30#[derive(Debug, Error)]
36pub enum WalError {
37 #[error("WAL capacity exceeded: current={current}, max={max}")]
39 CapacityExceeded { current: usize, max: usize },
40
41 #[error("Checkpoint failed: {0}")]
43 CheckpointFailed(String),
44
45 #[error("Serialization error: {0}")]
47 Serialization(String),
48}
49
50#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
56pub enum WalOp {
57 Put {
59 cid: String,
61 data_len: u64,
63 },
64 Delete {
66 cid: String,
68 },
69 BatchPut {
71 cids: Vec<String>,
73 total_bytes: u64,
75 },
76 Checkpoint {
78 sequence: u64,
80 },
81}
82
83impl WalOp {
84 fn to_bytes_for_crc(&self) -> Result<Vec<u8>, WalError> {
86 serde_json::to_vec(self).map_err(|e| WalError::Serialization(e.to_string()))
87 }
88}
89
90#[derive(Debug, Clone)]
96pub struct WalEntry {
97 pub sequence: u64,
99 pub op: WalOp,
101 pub crc32: u32,
103 pub timestamp_ms: u64,
105}
106
107#[derive(Debug, Default)]
113pub struct WalStats {
114 pub total_appended: AtomicU64,
116 pub total_checkpoints: AtomicU64,
118 pub total_truncated: AtomicU64,
120 pub total_replayed: AtomicU64,
122}
123
124#[derive(Debug, Clone, PartialEq)]
126pub struct WalStatsSnapshot {
127 pub total_appended: u64,
128 pub total_checkpoints: u64,
129 pub total_truncated: u64,
130 pub total_replayed: u64,
131}
132
133impl WalStats {
134 pub fn snapshot(&self) -> WalStatsSnapshot {
136 WalStatsSnapshot {
137 total_appended: self.total_appended.load(Ordering::Relaxed),
138 total_checkpoints: self.total_checkpoints.load(Ordering::Relaxed),
139 total_truncated: self.total_truncated.load(Ordering::Relaxed),
140 total_replayed: self.total_replayed.load(Ordering::Relaxed),
141 }
142 }
143}
144
145pub struct WriteAheadLog {
162 entries: Mutex<Vec<WalEntry>>,
163 next_sequence: AtomicU64,
164 checkpoint_sequence: AtomicU64,
165 pub max_entries: usize,
167 pub stats: WalStats,
169}
170
171impl WriteAheadLog {
172 pub fn new(max_entries: usize) -> Self {
174 Self {
175 entries: Mutex::new(Vec::new()),
176 next_sequence: AtomicU64::new(1),
177 checkpoint_sequence: AtomicU64::new(0),
178 max_entries,
179 stats: WalStats::default(),
180 }
181 }
182
183 pub fn with_defaults() -> Self {
185 Self::new(10_000)
186 }
187
188 fn now_ms() -> u64 {
190 use std::time::{SystemTime, UNIX_EPOCH};
191 SystemTime::now()
192 .duration_since(UNIX_EPOCH)
193 .unwrap_or_default()
194 .as_millis() as u64
195 }
196
197 pub fn append(&self, op: WalOp) -> Result<u64, WalError> {
206 let mut entries = self
207 .entries
208 .lock()
209 .map_err(|_| WalError::CheckpointFailed("lock poisoned during append".into()))?;
210
211 let current = entries.len();
212 if current >= self.max_entries {
213 return Err(WalError::CapacityExceeded {
214 current,
215 max: self.max_entries,
216 });
217 }
218
219 let crc32 = {
220 let bytes = op.to_bytes_for_crc()?;
221 fnv1a_32(&bytes)
222 };
223
224 let sequence = self.next_sequence.fetch_add(1, Ordering::SeqCst);
225
226 entries.push(WalEntry {
227 sequence,
228 op,
229 crc32,
230 timestamp_ms: Self::now_ms(),
231 });
232
233 self.stats.total_appended.fetch_add(1, Ordering::Relaxed);
234 Ok(sequence)
235 }
236
237 pub fn checkpoint(&self) -> Result<u64, WalError> {
241 let checkpoint_seq = self.next_sequence.load(Ordering::SeqCst);
244
245 let seq = self.append(WalOp::Checkpoint {
246 sequence: checkpoint_seq,
247 })?;
248
249 self.checkpoint_sequence.store(seq, Ordering::SeqCst);
250 self.stats.total_checkpoints.fetch_add(1, Ordering::Relaxed);
251 Ok(seq)
252 }
253
254 pub fn entries_since_checkpoint(&self) -> Vec<WalEntry> {
257 let checkpoint = self.checkpoint_sequence.load(Ordering::SeqCst);
258 let entries = self.entries.lock().unwrap_or_else(|e| e.into_inner());
259 entries
260 .iter()
261 .filter(|e| e.sequence > checkpoint)
262 .cloned()
263 .collect()
264 }
265
266 pub fn truncate_before(&self, sequence: u64) -> usize {
269 let mut entries = self.entries.lock().unwrap_or_else(|e| e.into_inner());
270 let before = entries.len();
271 entries.retain(|e| e.sequence >= sequence);
272 let removed = before - entries.len();
273 self.stats
274 .total_truncated
275 .fetch_add(removed as u64, Ordering::Relaxed);
276 removed
277 }
278
279 pub fn entry_count(&self) -> usize {
281 let entries = self.entries.lock().unwrap_or_else(|e| e.into_inner());
282 entries.len()
283 }
284
285 pub fn replay_ops(&self) -> Vec<WalOp> {
288 let ops: Vec<WalOp> = self
289 .entries_since_checkpoint()
290 .into_iter()
291 .filter_map(|e| match e.op {
292 WalOp::Checkpoint { .. } => None,
293 other => Some(other),
294 })
295 .collect();
296
297 self.stats
298 .total_replayed
299 .fetch_add(ops.len() as u64, Ordering::Relaxed);
300 ops
301 }
302
303 pub fn checkpoint_sequence(&self) -> u64 {
305 self.checkpoint_sequence.load(Ordering::SeqCst)
306 }
307
308 pub fn next_sequence(&self) -> u64 {
311 self.next_sequence.load(Ordering::SeqCst)
312 }
313}
314
315impl std::fmt::Debug for WriteAheadLog {
316 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
317 f.debug_struct("WriteAheadLog")
318 .field("max_entries", &self.max_entries)
319 .field("next_sequence", &self.next_sequence.load(Ordering::Relaxed))
320 .field(
321 "checkpoint_sequence",
322 &self.checkpoint_sequence.load(Ordering::Relaxed),
323 )
324 .field("entry_count", &self.entry_count())
325 .finish()
326 }
327}
328
329#[derive(Debug, Clone, Copy, PartialEq, Eq)]
335pub enum WalEntryKind {
336 Put,
338 Delete,
340 Update,
342}
343
344#[derive(Debug, Clone)]
346pub struct StorageWalEntry {
347 pub sequence: u64,
349 pub kind: WalEntryKind,
351 pub key: String,
353 pub data: Vec<u8>,
355 pub timestamp_tick: u64,
357}
358
359#[derive(Debug, Clone)]
361pub struct WalConfig {
362 pub max_entries: usize,
364 pub max_data_bytes: u64,
366 pub checkpoint_interval: u64,
368}
369
370impl Default for WalConfig {
371 fn default() -> Self {
372 Self {
373 max_entries: 10_000,
374 max_data_bytes: 100_000_000, checkpoint_interval: 50,
376 }
377 }
378}
379
380#[derive(Debug, Clone)]
382pub struct StorageWalStats {
383 pub entry_count: usize,
385 pub data_bytes: u64,
387 pub checkpoint_count: usize,
389 pub next_sequence: u64,
391 pub oldest_sequence: Option<u64>,
393}
394
395pub struct StorageWriteAheadLog {
402 config: WalConfig,
403 entries: VecDeque<StorageWalEntry>,
404 next_sequence: u64,
405 current_tick: u64,
406 total_data_bytes: u64,
407 checkpoints: Vec<u64>,
408 last_checkpoint_tick: u64,
409}
410
411impl StorageWriteAheadLog {
412 pub fn new(config: WalConfig) -> Self {
414 Self {
415 config,
416 entries: VecDeque::new(),
417 next_sequence: 1,
418 current_tick: 0,
419 total_data_bytes: 0,
420 checkpoints: Vec::new(),
421 last_checkpoint_tick: 0,
422 }
423 }
424
425 pub fn append(&mut self, kind: WalEntryKind, key: &str, data: Vec<u8>) -> Result<u64, String> {
433 let new_data_len = data.len() as u64;
434
435 let would_be = self.total_data_bytes + new_data_len;
438 if would_be > self.config.max_data_bytes {
439 return Err(format!(
440 "WAL data limit exceeded: would be {} bytes, max {} bytes",
441 would_be, self.config.max_data_bytes
442 ));
443 }
444
445 if self.entries.len() >= self.config.max_entries {
447 if let Some(evicted) = self.entries.pop_front() {
448 self.total_data_bytes = self
449 .total_data_bytes
450 .saturating_sub(evicted.data.len() as u64);
451 }
452 }
453
454 let seq = self.next_sequence;
455 self.next_sequence += 1;
456
457 self.entries.push_back(StorageWalEntry {
458 sequence: seq,
459 kind,
460 key: key.to_string(),
461 data,
462 timestamp_tick: self.current_tick,
463 });
464 self.total_data_bytes += new_data_len;
465
466 Ok(seq)
467 }
468
469 pub fn get_entry(&self, sequence: u64) -> Option<&StorageWalEntry> {
471 let front_seq = self.entries.front().map(|e| e.sequence)?;
474 if sequence < front_seq {
475 return None;
476 }
477 let idx = (sequence - front_seq) as usize;
478 self.entries.get(idx).filter(|e| e.sequence == sequence)
479 }
480
481 pub fn entries_since(&self, sequence: u64) -> Vec<&StorageWalEntry> {
484 self.entries
485 .iter()
486 .filter(|e| e.sequence > sequence)
487 .collect()
488 }
489
490 pub fn create_checkpoint(&mut self) -> u64 {
492 let cp_seq = if let Some(last) = self.entries.back() {
493 last.sequence
494 } else {
495 self.next_sequence.saturating_sub(1)
496 };
497 self.checkpoints.push(cp_seq);
498 self.last_checkpoint_tick = self.current_tick;
499 cp_seq
500 }
501
502 pub fn truncate_before(&mut self, sequence: u64) {
505 while let Some(front) = self.entries.front() {
506 if front.sequence < sequence {
507 let evicted = self
508 .entries
509 .pop_front()
510 .expect("front existed in condition");
511 self.total_data_bytes = self
512 .total_data_bytes
513 .saturating_sub(evicted.data.len() as u64);
514 } else {
515 break;
516 }
517 }
518 }
519
520 pub fn replay_from_checkpoint(
525 &self,
526 checkpoint_idx: usize,
527 ) -> Result<Vec<&StorageWalEntry>, String> {
528 let cp_seq = self.checkpoints.get(checkpoint_idx).ok_or_else(|| {
529 format!(
530 "checkpoint index {} out of range (have {})",
531 checkpoint_idx,
532 self.checkpoints.len()
533 )
534 })?;
535 Ok(self.entries_since(*cp_seq))
536 }
537
538 pub fn should_checkpoint(&self) -> bool {
541 self.current_tick.saturating_sub(self.last_checkpoint_tick)
542 >= self.config.checkpoint_interval
543 }
544
545 pub fn tick(&mut self) {
547 self.current_tick += 1;
548 }
549
550 pub fn entry_count(&self) -> usize {
552 self.entries.len()
553 }
554
555 pub fn data_bytes(&self) -> u64 {
557 self.total_data_bytes
558 }
559
560 pub fn stats(&self) -> StorageWalStats {
562 StorageWalStats {
563 entry_count: self.entries.len(),
564 data_bytes: self.total_data_bytes,
565 checkpoint_count: self.checkpoints.len(),
566 next_sequence: self.next_sequence,
567 oldest_sequence: self.entries.front().map(|e| e.sequence),
568 }
569 }
570}
571
572impl std::fmt::Debug for StorageWriteAheadLog {
573 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
574 f.debug_struct("StorageWriteAheadLog")
575 .field("next_sequence", &self.next_sequence)
576 .field("current_tick", &self.current_tick)
577 .field("entry_count", &self.entries.len())
578 .field("total_data_bytes", &self.total_data_bytes)
579 .field("checkpoint_count", &self.checkpoints.len())
580 .finish()
581 }
582}
583
584#[cfg(test)]
589mod tests {
590 use super::*;
591
592 fn make_put(cid: &str, data_len: u64) -> WalOp {
597 WalOp::Put {
598 cid: cid.to_string(),
599 data_len,
600 }
601 }
602
603 fn make_delete(cid: &str) -> WalOp {
604 WalOp::Delete {
605 cid: cid.to_string(),
606 }
607 }
608
609 fn make_batch_put(cids: &[&str], total_bytes: u64) -> WalOp {
610 WalOp::BatchPut {
611 cids: cids.iter().map(|s| s.to_string()).collect(),
612 total_bytes,
613 }
614 }
615
616 #[test]
621 fn test_append_single_entry_sequence_starts_at_one() {
622 let wal = WriteAheadLog::new(100);
623 let seq = wal.append(make_put("bafya", 64)).expect("append failed");
624 assert_eq!(seq, 1, "first sequence should be 1");
625 assert_eq!(wal.entry_count(), 1);
626 }
627
628 #[test]
629 fn test_append_multiple_entries_sequence_increments() {
630 let wal = WriteAheadLog::new(100);
631 let s1 = wal.append(make_put("cid1", 10)).expect("append 1");
632 let s2 = wal.append(make_delete("cid1")).expect("append 2");
633 let s3 = wal.append(make_put("cid2", 20)).expect("append 3");
634
635 assert_eq!(s1, 1);
636 assert_eq!(s2, 2);
637 assert_eq!(s3, 3);
638 assert_eq!(wal.entry_count(), 3);
639 }
640
641 #[test]
646 fn test_append_exceeds_max_entries_returns_error() {
647 let wal = WriteAheadLog::new(2);
648 wal.append(make_put("c1", 1)).expect("first append");
649 wal.append(make_put("c2", 2)).expect("second append");
650
651 let result = wal.append(make_put("c3", 3));
652 match result {
653 Err(WalError::CapacityExceeded { current, max }) => {
654 assert_eq!(current, 2);
655 assert_eq!(max, 2);
656 }
657 other => panic!("expected CapacityExceeded, got {:?}", other),
658 }
659 }
660
661 #[test]
666 fn test_checkpoint_records_sequence() {
667 let wal = WriteAheadLog::new(100);
668 wal.append(make_put("x", 1)).expect("append");
669 wal.append(make_put("y", 2)).expect("append");
670
671 let cp_seq = wal.checkpoint().expect("checkpoint");
672 assert!(cp_seq >= 3, "checkpoint entry should get next sequence (3)");
673 assert_eq!(wal.checkpoint_sequence(), cp_seq);
674 }
675
676 #[test]
681 fn test_entries_since_checkpoint_returns_only_new() {
682 let wal = WriteAheadLog::new(100);
683 wal.append(make_put("before", 1)).expect("append");
684 wal.checkpoint().expect("checkpoint");
685
686 wal.append(make_put("after1", 2)).expect("append after");
687 wal.append(make_put("after2", 3)).expect("append after");
688
689 let since = wal.entries_since_checkpoint();
690 assert_eq!(since.len(), 2);
691 for entry in &since {
692 match &entry.op {
693 WalOp::Put { cid, .. } => {
694 assert!(cid.starts_with("after"), "unexpected CID: {}", cid);
695 }
696 other => panic!("unexpected op: {:?}", other),
697 }
698 }
699 }
700
701 #[test]
706 fn test_entries_since_checkpoint_empty_after_fresh_checkpoint() {
707 let wal = WriteAheadLog::new(100);
708 wal.append(make_put("a", 1)).expect("a");
709 wal.append(make_put("b", 2)).expect("b");
710 wal.checkpoint().expect("checkpoint");
711
712 let since = wal.entries_since_checkpoint();
716 assert!(
717 since.is_empty(),
718 "expected empty, got {} entries",
719 since.len()
720 );
721 }
722
723 #[test]
728 fn test_truncate_before_removes_correct_entries() {
729 let wal = WriteAheadLog::new(100);
730 for i in 0..5u64 {
731 wal.append(make_put(&format!("c{i}"), i)).expect("append");
732 }
733 let removed = wal.truncate_before(3);
735 assert_eq!(removed, 2, "should remove exactly 2 entries");
736 assert_eq!(wal.entry_count(), 3, "3 entries should remain");
737 }
738
739 #[test]
740 fn test_truncate_before_nothing_to_remove() {
741 let wal = WriteAheadLog::new(100);
742 wal.append(make_put("a", 1)).expect("append");
743 let removed = wal.truncate_before(1); assert_eq!(removed, 0);
745 assert_eq!(wal.entry_count(), 1);
746 }
747
748 #[test]
753 fn test_replay_ops_excludes_checkpoint_ops() {
754 let wal = WriteAheadLog::new(100);
755 wal.append(make_put("old", 1)).expect("append old");
757 wal.checkpoint().expect("cp1");
758
759 wal.append(make_put("new1", 2)).expect("new1");
761 wal.append(make_delete("new1")).expect("del");
762 wal.checkpoint().expect("cp2");
763
764 let ops = wal.replay_ops();
766 assert!(
767 ops.is_empty(),
768 "after second checkpoint there are no data ops to replay, got {:?}",
769 ops
770 );
771 }
772
773 #[test]
774 fn test_replay_ops_returns_data_ops_since_checkpoint() {
775 let wal = WriteAheadLog::new(100);
776 wal.append(make_put("a", 1)).expect("a");
777 wal.checkpoint().expect("cp");
778
779 wal.append(make_put("b", 2)).expect("b");
780 wal.append(make_delete("a")).expect("del a");
781 wal.append(make_batch_put(&["c", "d"], 500)).expect("batch");
782
783 let ops = wal.replay_ops();
784 assert_eq!(ops.len(), 3);
785 assert!(matches!(ops[0], WalOp::Put { ref cid, .. } if cid == "b"));
786 assert!(matches!(ops[1], WalOp::Delete { ref cid } if cid == "a"));
787 assert!(matches!(ops[2], WalOp::BatchPut { .. }));
788 }
789
790 #[test]
795 fn test_crc32_non_zero_for_non_empty_data() {
796 let data = b"hello ipfrs wal";
797 let crc = fnv1a_32(data);
798 assert_ne!(
799 crc, 0,
800 "FNV-1a should produce non-zero hash for non-empty input"
801 );
802 }
803
804 #[test]
805 fn test_crc32_entry_stored_correctly() {
806 let wal = WriteAheadLog::new(100);
807 let op = make_put("bafytest123", 256);
808 let seq = wal.append(op.clone()).expect("append");
809
810 let entries = wal
811 .entries_since_checkpoint()
812 .into_iter()
813 .find(|e| e.sequence == seq)
814 .expect("entry not found");
815
816 let expected_crc = fnv1a_32(&serde_json::to_vec(&op).unwrap());
817 assert_eq!(entries.crc32, expected_crc);
818 assert_ne!(entries.crc32, 0);
819 }
820
821 #[test]
826 fn test_stats_total_appended() {
827 let wal = WriteAheadLog::new(100);
828 wal.append(make_put("x", 1)).expect("x");
829 wal.append(make_delete("x")).expect("del x");
830 let snap = wal.stats.snapshot();
831 assert_eq!(snap.total_appended, 2);
833 }
834
835 #[test]
836 fn test_stats_total_checkpoints() {
837 let wal = WriteAheadLog::new(100);
838 wal.append(make_put("a", 1)).expect("a");
839 wal.checkpoint().expect("cp1");
840 wal.checkpoint().expect("cp2");
841 let snap = wal.stats.snapshot();
842 assert_eq!(snap.total_checkpoints, 2);
843 assert_eq!(snap.total_appended, 3);
845 }
846
847 #[test]
848 fn test_stats_total_truncated() {
849 let wal = WriteAheadLog::new(100);
850 for i in 0..6u64 {
851 wal.append(make_put(&format!("c{i}"), i)).expect("append");
852 }
853 wal.truncate_before(4); let snap = wal.stats.snapshot();
855 assert_eq!(snap.total_truncated, 3);
856 }
857
858 #[test]
859 fn test_stats_total_replayed() {
860 let wal = WriteAheadLog::new(100);
861 wal.append(make_put("a", 1)).expect("a");
862 wal.checkpoint().expect("cp");
863 wal.append(make_put("b", 2)).expect("b");
864 wal.append(make_delete("a")).expect("del");
865
866 let ops = wal.replay_ops();
867 assert_eq!(ops.len(), 2);
868
869 let snap = wal.stats.snapshot();
870 assert_eq!(snap.total_replayed, 2);
871 }
872
873 #[test]
878 fn test_multiple_op_types_roundtrip() {
879 let wal = WriteAheadLog::new(100);
880
881 let s1 = wal.append(make_put("cid-put-1", 100)).expect("put1");
882 let s2 = wal.append(make_put("cid-put-2", 200)).expect("put2");
883 let s3 = wal
884 .append(make_batch_put(&["cid-a", "cid-b", "cid-c"], 750))
885 .expect("batch");
886 let s4 = wal.append(make_delete("cid-put-1")).expect("del");
887
888 assert!(s1 < s2 && s2 < s3 && s3 < s4, "sequences must be monotonic");
889 assert_eq!(wal.entry_count(), 4);
890
891 let ops = wal.replay_ops();
892 assert_eq!(ops.len(), 4);
893
894 assert!(matches!(&ops[0], WalOp::Put { cid, data_len: 100 } if cid == "cid-put-1"));
895 assert!(matches!(&ops[1], WalOp::Put { cid, data_len: 200 } if cid == "cid-put-2"));
896 assert!(matches!(&ops[2], WalOp::BatchPut { cids, total_bytes: 750 } if cids.len() == 3));
897 assert!(matches!(&ops[3], WalOp::Delete { cid } if cid == "cid-put-1"));
898 }
899
900 #[test]
905 fn test_fnv1a_deterministic() {
906 let data = b"ipfrs-wal";
907 let h1 = fnv1a_32(data);
908 let h2 = fnv1a_32(data);
909 assert_eq!(h1, h2, "FNV-1a must be deterministic");
910 }
911
912 #[test]
913 fn test_fnv1a_empty_input_is_offset_basis() {
914 let h = fnv1a_32(b"");
915 assert_eq!(h, 2_166_136_261u32, "empty input should equal offset basis");
916 }
917
918 #[test]
923 fn test_concurrent_appends_unique_sequences() {
924 use std::sync::Arc;
925 use std::thread;
926
927 let wal = Arc::new(WriteAheadLog::new(10_000));
928 let threads = 8usize;
929 let per_thread = 50usize;
930
931 let handles: Vec<_> = (0..threads)
932 .map(|t| {
933 let w = Arc::clone(&wal);
934 thread::spawn(move || {
935 (0..per_thread)
936 .map(|i| {
937 w.append(make_put(&format!("t{t}-c{i}"), i as u64))
938 .expect("concurrent append")
939 })
940 .collect::<Vec<_>>()
941 })
942 })
943 .collect();
944
945 let mut all_seqs: Vec<u64> = handles
946 .into_iter()
947 .flat_map(|h| h.join().expect("thread panicked"))
948 .collect();
949
950 all_seqs.sort_unstable();
951 all_seqs.dedup();
952 assert_eq!(
953 all_seqs.len(),
954 threads * per_thread,
955 "all sequences must be unique"
956 );
957 assert_eq!(wal.entry_count(), threads * per_thread);
958 }
959
960 #[test]
965 fn test_debug_impl_does_not_panic() {
966 let wal = WriteAheadLog::with_defaults();
967 wal.append(make_put("dbg", 1)).expect("append");
968 let _ = format!("{:?}", wal);
969 }
970
971 #[test]
976 fn test_wal_error_display() {
977 let e = WalError::CapacityExceeded { current: 5, max: 5 };
978 let msg = e.to_string();
979 assert!(
980 msg.contains("current=5") && msg.contains("max=5"),
981 "{}",
982 msg
983 );
984
985 let e2 = WalError::CheckpointFailed("disk full".into());
986 assert!(e2.to_string().contains("disk full"));
987 }
988
989 fn swal_config() -> WalConfig {
994 WalConfig {
995 max_entries: 100,
996 max_data_bytes: 10_000,
997 checkpoint_interval: 5,
998 }
999 }
1000
1001 fn swal_default() -> StorageWriteAheadLog {
1002 StorageWriteAheadLog::new(swal_config())
1003 }
1004
1005 #[test]
1010 fn test_swal_append_sequence_starts_at_one() {
1011 let mut wal = swal_default();
1012 let seq = wal
1013 .append(WalEntryKind::Put, "key1", vec![1, 2, 3])
1014 .expect("append");
1015 assert_eq!(seq, 1);
1016 assert_eq!(wal.entry_count(), 1);
1017 }
1018
1019 #[test]
1020 fn test_swal_append_multiple_sequences_increment() {
1021 let mut wal = swal_default();
1022 let s1 = wal.append(WalEntryKind::Put, "k1", vec![1]).expect("s1");
1023 let s2 = wal.append(WalEntryKind::Delete, "k1", vec![]).expect("s2");
1024 let s3 = wal.append(WalEntryKind::Update, "k2", vec![9]).expect("s3");
1025 assert_eq!(s1, 1);
1026 assert_eq!(s2, 2);
1027 assert_eq!(s3, 3);
1028 assert_eq!(wal.entry_count(), 3);
1029 }
1030
1031 #[test]
1036 fn test_swal_get_entry_existing() {
1037 let mut wal = swal_default();
1038 let seq = wal.append(WalEntryKind::Put, "abc", vec![10]).expect("a");
1039 let entry = wal.get_entry(seq).expect("entry should exist");
1040 assert_eq!(entry.key, "abc");
1041 assert_eq!(entry.data, vec![10]);
1042 assert_eq!(entry.kind, WalEntryKind::Put);
1043 }
1044
1045 #[test]
1046 fn test_swal_get_entry_missing() {
1047 let wal = swal_default();
1048 assert!(wal.get_entry(999).is_none());
1049 }
1050
1051 #[test]
1052 fn test_swal_get_entry_after_truncation() {
1053 let mut wal = swal_default();
1054 wal.append(WalEntryKind::Put, "a", vec![1]).expect("a");
1055 wal.append(WalEntryKind::Put, "b", vec![2]).expect("b");
1056 wal.truncate_before(2);
1057 assert!(wal.get_entry(1).is_none());
1058 assert!(wal.get_entry(2).is_some());
1059 }
1060
1061 #[test]
1066 fn test_swal_entries_since() {
1067 let mut wal = swal_default();
1068 for i in 0..5 {
1069 wal.append(WalEntryKind::Put, &format!("k{i}"), vec![i as u8])
1070 .expect("append");
1071 }
1072 let since = wal.entries_since(3);
1073 assert_eq!(since.len(), 2);
1074 assert_eq!(since[0].sequence, 4);
1075 assert_eq!(since[1].sequence, 5);
1076 }
1077
1078 #[test]
1079 fn test_swal_entries_since_zero_returns_all() {
1080 let mut wal = swal_default();
1081 wal.append(WalEntryKind::Put, "x", vec![]).expect("x");
1082 wal.append(WalEntryKind::Delete, "y", vec![]).expect("y");
1083 let all = wal.entries_since(0);
1084 assert_eq!(all.len(), 2);
1085 }
1086
1087 #[test]
1092 fn test_swal_create_checkpoint() {
1093 let mut wal = swal_default();
1094 wal.append(WalEntryKind::Put, "a", vec![1]).expect("a");
1095 wal.append(WalEntryKind::Put, "b", vec![2]).expect("b");
1096 let cp = wal.create_checkpoint();
1097 assert_eq!(cp, 2); assert_eq!(wal.stats().checkpoint_count, 1);
1099 }
1100
1101 #[test]
1102 fn test_swal_create_multiple_checkpoints() {
1103 let mut wal = swal_default();
1104 wal.append(WalEntryKind::Put, "a", vec![]).expect("a");
1105 let cp1 = wal.create_checkpoint();
1106 wal.append(WalEntryKind::Put, "b", vec![]).expect("b");
1107 wal.append(WalEntryKind::Put, "c", vec![]).expect("c");
1108 let cp2 = wal.create_checkpoint();
1109 assert!(cp2 > cp1);
1110 assert_eq!(wal.stats().checkpoint_count, 2);
1111 }
1112
1113 #[test]
1118 fn test_swal_truncate_before() {
1119 let mut wal = swal_default();
1120 wal.append(WalEntryKind::Put, "a", vec![1, 2, 3])
1121 .expect("a");
1122 wal.append(WalEntryKind::Put, "b", vec![4, 5]).expect("b");
1123 wal.append(WalEntryKind::Put, "c", vec![6]).expect("c");
1124
1125 let bytes_before = wal.data_bytes();
1126 assert_eq!(bytes_before, 6);
1127
1128 wal.truncate_before(3); assert_eq!(wal.entry_count(), 1);
1130 assert_eq!(wal.data_bytes(), 1); }
1132
1133 #[test]
1134 fn test_swal_truncate_before_nothing() {
1135 let mut wal = swal_default();
1136 wal.append(WalEntryKind::Put, "x", vec![1]).expect("x");
1137 wal.truncate_before(1); assert_eq!(wal.entry_count(), 1);
1139 }
1140
1141 #[test]
1142 fn test_swal_truncate_before_all() {
1143 let mut wal = swal_default();
1144 wal.append(WalEntryKind::Put, "a", vec![1]).expect("a");
1145 wal.append(WalEntryKind::Put, "b", vec![2]).expect("b");
1146 wal.truncate_before(100);
1147 assert_eq!(wal.entry_count(), 0);
1148 assert_eq!(wal.data_bytes(), 0);
1149 }
1150
1151 #[test]
1156 fn test_swal_replay_from_checkpoint() {
1157 let mut wal = swal_default();
1158 wal.append(WalEntryKind::Put, "old", vec![1]).expect("old");
1159 wal.create_checkpoint(); wal.append(WalEntryKind::Put, "new1", vec![2]).expect("n1");
1161 wal.append(WalEntryKind::Delete, "old", vec![]).expect("n2");
1162
1163 let replayed = wal.replay_from_checkpoint(0).expect("replay");
1164 assert_eq!(replayed.len(), 2);
1165 assert_eq!(replayed[0].key, "new1");
1166 assert_eq!(replayed[1].key, "old");
1167 }
1168
1169 #[test]
1170 fn test_swal_replay_from_checkpoint_invalid_index() {
1171 let wal = swal_default();
1172 let result = wal.replay_from_checkpoint(0);
1173 assert!(result.is_err());
1174 }
1175
1176 #[test]
1177 fn test_swal_replay_from_second_checkpoint() {
1178 let mut wal = swal_default();
1179 wal.append(WalEntryKind::Put, "a", vec![]).expect("a");
1180 wal.create_checkpoint(); wal.append(WalEntryKind::Put, "b", vec![]).expect("b");
1182 wal.create_checkpoint(); wal.append(WalEntryKind::Put, "c", vec![]).expect("c");
1184
1185 let replayed = wal.replay_from_checkpoint(1).expect("replay from cp1");
1186 assert_eq!(replayed.len(), 1);
1187 assert_eq!(replayed[0].key, "c");
1188 }
1189
1190 #[test]
1195 fn test_swal_max_entries_overflow_evicts_oldest() {
1196 let cfg = WalConfig {
1197 max_entries: 3,
1198 max_data_bytes: 100_000,
1199 checkpoint_interval: 50,
1200 };
1201 let mut wal = StorageWriteAheadLog::new(cfg);
1202 wal.append(WalEntryKind::Put, "a", vec![1]).expect("a");
1203 wal.append(WalEntryKind::Put, "b", vec![2]).expect("b");
1204 wal.append(WalEntryKind::Put, "c", vec![3]).expect("c");
1205 wal.append(WalEntryKind::Put, "d", vec![4]).expect("d");
1207
1208 assert_eq!(wal.entry_count(), 3);
1209 assert!(wal.get_entry(1).is_none(), "seq 1 should be evicted");
1210 assert!(wal.get_entry(2).is_some());
1211 assert!(wal.get_entry(4).is_some());
1212 }
1213
1214 #[test]
1219 fn test_swal_max_data_bytes_enforced() {
1220 let cfg = WalConfig {
1221 max_entries: 100,
1222 max_data_bytes: 10,
1223 checkpoint_interval: 50,
1224 };
1225 let mut wal = StorageWriteAheadLog::new(cfg);
1226 wal.append(WalEntryKind::Put, "a", vec![0; 8]).expect("a");
1227 let result = wal.append(WalEntryKind::Put, "b", vec![0; 5]);
1229 assert!(result.is_err());
1230 assert!(result
1231 .as_ref()
1232 .err()
1233 .is_some_and(|e| e.contains("data limit exceeded")));
1234 }
1235
1236 #[test]
1237 fn test_swal_max_data_bytes_exact_limit() {
1238 let cfg = WalConfig {
1239 max_entries: 100,
1240 max_data_bytes: 10,
1241 checkpoint_interval: 50,
1242 };
1243 let mut wal = StorageWriteAheadLog::new(cfg);
1244 wal.append(WalEntryKind::Put, "a", vec![0; 10])
1246 .expect("exact limit");
1247 assert_eq!(wal.data_bytes(), 10);
1248 let result = wal.append(WalEntryKind::Put, "b", vec![1]);
1250 assert!(result.is_err());
1251 }
1252
1253 #[test]
1258 fn test_swal_should_checkpoint_timing() {
1259 let cfg = WalConfig {
1260 max_entries: 100,
1261 max_data_bytes: 100_000,
1262 checkpoint_interval: 3,
1263 };
1264 let mut wal = StorageWriteAheadLog::new(cfg);
1265 assert!(!wal.should_checkpoint());
1268 wal.tick(); assert!(!wal.should_checkpoint());
1270 wal.tick(); assert!(!wal.should_checkpoint());
1272 wal.tick(); assert!(wal.should_checkpoint());
1274
1275 wal.create_checkpoint(); assert!(!wal.should_checkpoint());
1277 wal.tick(); wal.tick(); wal.tick(); assert!(wal.should_checkpoint());
1281 }
1282
1283 #[test]
1288 fn test_swal_stats_accuracy() {
1289 let mut wal = swal_default();
1290 wal.append(WalEntryKind::Put, "a", vec![1, 2]).expect("a");
1291 wal.append(WalEntryKind::Delete, "b", vec![]).expect("b");
1292 wal.create_checkpoint();
1293
1294 let s = wal.stats();
1295 assert_eq!(s.entry_count, 2);
1296 assert_eq!(s.data_bytes, 2);
1297 assert_eq!(s.checkpoint_count, 1);
1298 assert_eq!(s.next_sequence, 3);
1299 assert_eq!(s.oldest_sequence, Some(1));
1300 }
1301
1302 #[test]
1303 fn test_swal_stats_after_truncate() {
1304 let mut wal = swal_default();
1305 wal.append(WalEntryKind::Put, "a", vec![1]).expect("a");
1306 wal.append(WalEntryKind::Put, "b", vec![2, 3]).expect("b");
1307 wal.truncate_before(2);
1308 let s = wal.stats();
1309 assert_eq!(s.entry_count, 1);
1310 assert_eq!(s.data_bytes, 2);
1311 assert_eq!(s.oldest_sequence, Some(2));
1312 }
1313
1314 #[test]
1319 fn test_swal_empty_wal() {
1320 let wal = swal_default();
1321 assert_eq!(wal.entry_count(), 0);
1322 assert_eq!(wal.data_bytes(), 0);
1323 assert!(wal.get_entry(1).is_none());
1324 assert!(wal.entries_since(0).is_empty());
1325 let s = wal.stats();
1326 assert_eq!(s.entry_count, 0);
1327 assert_eq!(s.oldest_sequence, None);
1328 assert_eq!(s.checkpoint_count, 0);
1329 }
1330
1331 #[test]
1336 fn test_swal_mixed_entry_kinds() {
1337 let mut wal = swal_default();
1338 wal.append(WalEntryKind::Put, "file1", vec![10, 20])
1339 .expect("put");
1340 wal.append(WalEntryKind::Update, "file1", vec![30, 40])
1341 .expect("update");
1342 wal.append(WalEntryKind::Delete, "file1", vec![])
1343 .expect("delete");
1344
1345 assert_eq!(wal.entry_count(), 3);
1346 let e1 = wal.get_entry(1).expect("e1");
1347 assert_eq!(e1.kind, WalEntryKind::Put);
1348 let e2 = wal.get_entry(2).expect("e2");
1349 assert_eq!(e2.kind, WalEntryKind::Update);
1350 let e3 = wal.get_entry(3).expect("e3");
1351 assert_eq!(e3.kind, WalEntryKind::Delete);
1352 }
1353
1354 #[test]
1359 fn test_swal_tick_advances_clock() {
1360 let mut wal = swal_default();
1361 wal.tick();
1362 wal.tick();
1363 wal.append(WalEntryKind::Put, "t", vec![]).expect("t");
1364 let entry = wal.get_entry(1).expect("entry");
1365 assert_eq!(entry.timestamp_tick, 2);
1366 }
1367
1368 #[test]
1373 fn test_swal_data_bytes_tracking() {
1374 let mut wal = swal_default();
1375 wal.append(WalEntryKind::Put, "a", vec![0; 100]).expect("a");
1376 assert_eq!(wal.data_bytes(), 100);
1377 wal.append(WalEntryKind::Put, "b", vec![0; 50]).expect("b");
1378 assert_eq!(wal.data_bytes(), 150);
1379 wal.truncate_before(2);
1380 assert_eq!(wal.data_bytes(), 50);
1381 }
1382
1383 #[test]
1388 fn test_swal_overflow_eviction_updates_data_bytes() {
1389 let cfg = WalConfig {
1390 max_entries: 2,
1391 max_data_bytes: 100_000,
1392 checkpoint_interval: 50,
1393 };
1394 let mut wal = StorageWriteAheadLog::new(cfg);
1395 wal.append(WalEntryKind::Put, "a", vec![0; 30]).expect("a");
1396 wal.append(WalEntryKind::Put, "b", vec![0; 20]).expect("b");
1397 assert_eq!(wal.data_bytes(), 50);
1398
1399 wal.append(WalEntryKind::Put, "c", vec![0; 10]).expect("c");
1401 assert_eq!(wal.data_bytes(), 30); assert_eq!(wal.entry_count(), 2);
1403 }
1404
1405 #[test]
1410 fn test_swal_checkpoint_on_empty() {
1411 let mut wal = swal_default();
1412 let cp = wal.create_checkpoint();
1413 assert_eq!(cp, 0); assert_eq!(wal.stats().checkpoint_count, 1);
1415 }
1416
1417 #[test]
1422 fn test_swal_entries_since_none() {
1423 let mut wal = swal_default();
1424 wal.append(WalEntryKind::Put, "a", vec![]).expect("a");
1425 let since = wal.entries_since(100);
1426 assert!(since.is_empty());
1427 }
1428
1429 #[test]
1434 fn test_swal_default_config() {
1435 let cfg = WalConfig::default();
1436 assert_eq!(cfg.max_entries, 10_000);
1437 assert_eq!(cfg.max_data_bytes, 100_000_000);
1438 assert_eq!(cfg.checkpoint_interval, 50);
1439 }
1440
1441 #[test]
1446 fn test_swal_debug_does_not_panic() {
1447 let mut wal = swal_default();
1448 wal.append(WalEntryKind::Put, "dbg", vec![1]).expect("a");
1449 let dbg = format!("{:?}", wal);
1450 assert!(dbg.contains("StorageWriteAheadLog"));
1451 }
1452
1453 #[test]
1458 fn test_swal_entry_kind_equality() {
1459 assert_eq!(WalEntryKind::Put, WalEntryKind::Put);
1460 assert_ne!(WalEntryKind::Put, WalEntryKind::Delete);
1461 assert_ne!(WalEntryKind::Delete, WalEntryKind::Update);
1462 }
1463
1464 #[test]
1469 fn test_swal_entry_clone() {
1470 let entry = StorageWalEntry {
1471 sequence: 42,
1472 kind: WalEntryKind::Update,
1473 key: "cloned".to_string(),
1474 data: vec![7, 8, 9],
1475 timestamp_tick: 10,
1476 };
1477 let cloned = entry.clone();
1478 assert_eq!(cloned.sequence, 42);
1479 assert_eq!(cloned.kind, WalEntryKind::Update);
1480 assert_eq!(cloned.key, "cloned");
1481 assert_eq!(cloned.data, vec![7, 8, 9]);
1482 }
1483
1484 #[test]
1489 fn test_swal_stats_clone_and_debug() {
1490 let mut wal = swal_default();
1491 wal.append(WalEntryKind::Put, "s", vec![1, 2, 3])
1492 .expect("s");
1493 let stats = wal.stats();
1494 let cloned = stats.clone();
1495 assert_eq!(cloned.entry_count, 1);
1496 let dbg = format!("{:?}", cloned);
1497 assert!(dbg.contains("entry_count"));
1498 }
1499
1500 #[test]
1505 fn test_swal_large_sequential_append_and_replay() {
1506 let cfg = WalConfig {
1507 max_entries: 500,
1508 max_data_bytes: 100_000,
1509 checkpoint_interval: 100,
1510 };
1511 let mut wal = StorageWriteAheadLog::new(cfg);
1512
1513 for i in 0..200 {
1514 wal.append(WalEntryKind::Put, &format!("key{i}"), vec![i as u8])
1515 .expect("append");
1516 }
1517 assert_eq!(wal.entry_count(), 200);
1518
1519 wal.create_checkpoint(); for i in 200..250 {
1521 wal.append(WalEntryKind::Put, &format!("key{i}"), vec![i as u8])
1522 .expect("append");
1523 }
1524
1525 let replayed = wal.replay_from_checkpoint(0).expect("replay");
1526 assert_eq!(replayed.len(), 50);
1527 }
1528
1529 #[test]
1534 fn test_swal_should_checkpoint_interval_zero() {
1535 let cfg = WalConfig {
1536 max_entries: 100,
1537 max_data_bytes: 100_000,
1538 checkpoint_interval: 0,
1539 };
1540 let wal = StorageWriteAheadLog::new(cfg);
1541 assert!(wal.should_checkpoint());
1543 }
1544
1545 #[test]
1550 fn test_swal_delete_entries_zero_data() {
1551 let mut wal = swal_default();
1552 wal.append(WalEntryKind::Delete, "gone", vec![])
1553 .expect("del");
1554 assert_eq!(wal.data_bytes(), 0);
1555 assert_eq!(wal.entry_count(), 1);
1556 }
1557}