Skip to main content

ipfrs_storage/
wal_replay.rs

1//! WAL Replay Engine — crash recovery and state reconstruction from WAL entries.
2//!
3//! [`StorageWALReplay`] replays a sequence of [`WalEntry`] records to reconstruct
4//! in-memory storage state, supporting full replay, checkpoint-scoped replay,
5//! sequence-bounded replay, and transactional semantics (Begin/Commit/Rollback).
6
7use std::collections::{HashMap, VecDeque};
8
9// ---------------------------------------------------------------------------
10// FNV-1a 32-bit checksum (standalone, no external dep)
11// ---------------------------------------------------------------------------
12
13/// FNV-1a 32-bit hash used for WAL entry checksums.
14#[inline]
15fn fnv1a_32_bytes(data: &[u8]) -> u32 {
16    const OFFSET_BASIS: u32 = 2_166_136_261;
17    const PRIME: u32 = 16_777_619;
18    let mut h = OFFSET_BASIS;
19    for &b in data {
20        h ^= u32::from(b);
21        h = h.wrapping_mul(PRIME);
22    }
23    h
24}
25
26// ---------------------------------------------------------------------------
27// WalEntryType
28// ---------------------------------------------------------------------------
29
30/// Discriminates the semantic role of a [`WalEntry`].
31#[derive(Debug, Clone, PartialEq, Eq, Hash)]
32pub enum WalEntryType {
33    /// Write a key-value pair.
34    Put,
35    /// Remove a key.
36    Delete,
37    /// Mark a durable checkpoint that replay may start from.
38    Checkpoint,
39    /// Open a new transaction.
40    Begin,
41    /// Commit an open transaction, flushing its buffered ops.
42    Commit,
43    /// Rollback an open transaction, discarding its buffered ops.
44    Rollback,
45}
46
47// ---------------------------------------------------------------------------
48// WalEntry
49// ---------------------------------------------------------------------------
50
51/// A single record in the write-ahead log.
52#[derive(Debug, Clone)]
53pub struct WalEntry {
54    /// Monotonically increasing, globally unique sequence number.
55    pub sequence: u64,
56    /// Semantic type of this entry.
57    pub entry_type: WalEntryType,
58    /// Key this entry addresses (empty for Begin/Commit/Rollback).
59    pub key: String,
60    /// Optional payload for Put operations; `None` for Delete / control entries.
61    pub value: Option<Vec<u8>>,
62    /// Transaction this entry belongs to, if any.
63    pub transaction_id: Option<u64>,
64    /// FNV-1a 32-bit checksum over `(sequence, key, value)`.
65    pub checksum: u32,
66}
67
68impl WalEntry {
69    /// Computes FNV-1a 32-bit checksum over `sequence || key || value`.
70    pub fn compute_checksum(seq: u64, key: &str, value: &Option<Vec<u8>>) -> u32 {
71        let mut buf: Vec<u8> =
72            Vec::with_capacity(8 + key.len() + value.as_deref().map_or(0, |v| v.len()));
73        buf.extend_from_slice(&seq.to_le_bytes());
74        buf.extend_from_slice(key.as_bytes());
75        if let Some(v) = value {
76            buf.extend_from_slice(v);
77        }
78        fnv1a_32_bytes(&buf)
79    }
80
81    /// Returns `true` iff the stored checksum matches the recomputed one.
82    pub fn is_valid(&self) -> bool {
83        self.checksum == Self::compute_checksum(self.sequence, &self.key, &self.value)
84    }
85}
86
87// ---------------------------------------------------------------------------
88// ReplayPolicy
89// ---------------------------------------------------------------------------
90
91/// Controls which entries are included in a replay run.
92#[derive(Debug, Clone, PartialEq, Eq)]
93pub enum ReplayPolicy {
94    /// Process every entry from the oldest.
95    Full,
96    /// Start from the last Checkpoint entry (inclusive).
97    SinceCheckpoint,
98    /// Skip entries whose sequence number is strictly less than `s`.
99    SinceSequence(u64),
100    /// Replay only the last `n` entries.
101    LastN(usize),
102}
103
104// ---------------------------------------------------------------------------
105// ReplayStats
106// ---------------------------------------------------------------------------
107
108/// Counters collected during a single [`StorageWALReplay::replay`] invocation.
109#[derive(Debug, Clone, Default, PartialEq, Eq)]
110pub struct ReplayStats {
111    /// Total WAL entries examined.
112    pub entries_read: usize,
113    /// Entries whose effects were applied to the reconstructed state.
114    pub entries_applied: usize,
115    /// Entries skipped due to policy (before the replay window).
116    pub entries_skipped: usize,
117    /// Checkpoint entries encountered in the replay window.
118    pub checkpoints_found: usize,
119    /// Entries with a bad checksum that were discarded.
120    pub invalid_entries: usize,
121    /// Sequence number of the last applied entry (0 if none applied).
122    pub final_sequence: u64,
123}
124
125// ---------------------------------------------------------------------------
126// ReplayState
127// ---------------------------------------------------------------------------
128
129/// The reconstructed storage state produced by [`StorageWALReplay::replay`].
130#[derive(Debug, Clone, Default)]
131pub struct ReplayState {
132    /// Final key-value store after applying all replayed entries.
133    pub store: HashMap<String, Vec<u8>>,
134    /// Sequence number of the last entry that was applied.
135    pub sequence: u64,
136    /// Transactions still open (Begin seen, no Commit/Rollback yet).
137    pub active_transactions: HashMap<u64, Vec<WalEntry>>,
138    /// Number of transactions successfully committed.
139    pub committed_count: u64,
140    /// Number of transactions rolled back.
141    pub rolled_back_count: u64,
142}
143
144// ---------------------------------------------------------------------------
145// WalStats
146// ---------------------------------------------------------------------------
147
148/// Aggregate statistics describing the current state of a [`StorageWALReplay`] log.
149#[derive(Debug, Clone, Default, PartialEq, Eq)]
150pub struct WalStats {
151    /// Total number of entries currently in the log.
152    pub total_entries: usize,
153    /// Number of Checkpoint entries in the log.
154    pub checkpoint_count: usize,
155    /// Sequence number of the oldest entry (0 if empty).
156    pub oldest_sequence: u64,
157    /// Sequence number of the newest entry (0 if empty).
158    pub newest_sequence: u64,
159    /// Rough byte estimate of all entries (keys + values + fixed overhead).
160    pub estimated_replay_size: usize,
161}
162
163// ---------------------------------------------------------------------------
164// StorageWALReplay
165// ---------------------------------------------------------------------------
166
167/// Write-ahead log replay engine.
168///
169/// Maintains an in-memory ring buffer of [`WalEntry`] records.  When the ring
170/// exceeds `max_entries` the oldest entry is evicted automatically.  Use
171/// [`replay`](Self::replay) to reconstruct storage state from the buffered
172/// entries according to a chosen [`ReplayPolicy`].
173pub struct StorageWALReplay {
174    /// Ring-buffer of WAL entries, oldest first.
175    entries: VecDeque<WalEntry>,
176    /// Maximum number of entries to retain.
177    max_entries: usize,
178    /// Next sequence number to assign.
179    next_sequence: u64,
180    /// Sequence numbers of all Checkpoint entries currently in the buffer.
181    checkpoint_sequences: Vec<u64>,
182}
183
184impl StorageWALReplay {
185    /// Creates a new [`StorageWALReplay`] that retains at most `max_entries`.
186    pub fn new(max_entries: usize) -> Self {
187        Self {
188            entries: VecDeque::new(),
189            max_entries: max_entries.max(1),
190            next_sequence: 1,
191            checkpoint_sequences: Vec::new(),
192        }
193    }
194
195    // -----------------------------------------------------------------------
196    // append
197    // -----------------------------------------------------------------------
198
199    /// Appends a new entry to the log and returns it.
200    ///
201    /// - Assigns the next monotonic sequence number.
202    /// - Computes and stores the FNV-1a checksum.
203    /// - If `entry_type` is [`WalEntryType::Checkpoint`], records the sequence
204    ///   in `checkpoint_sequences`.
205    /// - Evicts the oldest entry when the buffer is at capacity.
206    pub fn append(
207        &mut self,
208        entry_type: WalEntryType,
209        key: String,
210        value: Option<Vec<u8>>,
211        tx_id: Option<u64>,
212    ) -> WalEntry {
213        let seq = self.next_sequence;
214        self.next_sequence += 1;
215
216        let checksum = WalEntry::compute_checksum(seq, &key, &value);
217        let is_checkpoint = entry_type == WalEntryType::Checkpoint;
218
219        let entry = WalEntry {
220            sequence: seq,
221            entry_type,
222            key,
223            value,
224            transaction_id: tx_id,
225            checksum,
226        };
227
228        // Evict oldest if at capacity.
229        if self.entries.len() >= self.max_entries {
230            if let Some(evicted) = self.entries.pop_front() {
231                // Remove evicted checkpoint sequence if present.
232                self.checkpoint_sequences.retain(|&s| s != evicted.sequence);
233            }
234        }
235
236        if is_checkpoint {
237            self.checkpoint_sequences.push(seq);
238        }
239
240        self.entries.push_back(entry.clone());
241        entry
242    }
243
244    // -----------------------------------------------------------------------
245    // replay
246    // -----------------------------------------------------------------------
247
248    /// Reconstructs storage state by replaying entries according to `policy`.
249    ///
250    /// Returns `(state, stats)`.
251    pub fn replay(&self, policy: ReplayPolicy) -> (ReplayState, ReplayStats) {
252        let mut stats = ReplayStats::default();
253        let mut state = ReplayState::default();
254
255        // Collect the replay window as a Vec<&WalEntry> to avoid lifetime issues
256        // with boxed heterogeneous iterators.
257        let total = self.entries.len();
258        let window_entries: Vec<&WalEntry> = match policy {
259            ReplayPolicy::Full => {
260                stats.entries_read = total;
261                self.entries.iter().collect()
262            }
263            ReplayPolicy::SinceCheckpoint => {
264                // Find the last checkpoint sequence that still exists in the buffer.
265                let start_seq = self
266                    .checkpoint_sequences
267                    .iter()
268                    .copied()
269                    .rev()
270                    .find(|&s| self.entries.iter().any(|e| e.sequence == s))
271                    .unwrap_or(0);
272                let skip_count = self
273                    .entries
274                    .iter()
275                    .filter(|e| e.sequence < start_seq)
276                    .count();
277                stats.entries_skipped = skip_count;
278                stats.entries_read = total.saturating_sub(skip_count);
279                self.entries
280                    .iter()
281                    .filter(|e| e.sequence >= start_seq)
282                    .collect()
283            }
284            ReplayPolicy::SinceSequence(min_seq) => {
285                let skip_count = self.entries.iter().filter(|e| e.sequence < min_seq).count();
286                stats.entries_skipped = skip_count;
287                stats.entries_read = total.saturating_sub(skip_count);
288                self.entries
289                    .iter()
290                    .filter(|e| e.sequence >= min_seq)
291                    .collect()
292            }
293            ReplayPolicy::LastN(n) => {
294                let skip_count = total.saturating_sub(n);
295                let take = total.min(n);
296                stats.entries_skipped = skip_count;
297                stats.entries_read = take;
298                self.entries.iter().skip(skip_count).take(take).collect()
299            }
300        };
301
302        for entry in window_entries {
303            // Validate checksum; discard and count corrupt entries.
304            if !entry.is_valid() {
305                stats.invalid_entries += 1;
306                continue;
307            }
308
309            match &entry.entry_type {
310                WalEntryType::Checkpoint => {
311                    stats.checkpoints_found += 1;
312                    stats.entries_applied += 1;
313                    state.sequence = entry.sequence;
314                    stats.final_sequence = entry.sequence;
315                }
316                WalEntryType::Begin => {
317                    if let Some(tx_id) = entry.transaction_id {
318                        state.active_transactions.entry(tx_id).or_default();
319                    }
320                    stats.entries_applied += 1;
321                    state.sequence = entry.sequence;
322                    stats.final_sequence = entry.sequence;
323                }
324                WalEntryType::Commit => {
325                    if let Some(tx_id) = entry.transaction_id {
326                        if let Some(buffered) = state.active_transactions.remove(&tx_id) {
327                            for buffered_entry in buffered {
328                                match buffered_entry.entry_type {
329                                    WalEntryType::Put => {
330                                        if let Some(v) = buffered_entry.value {
331                                            state.store.insert(buffered_entry.key, v);
332                                        }
333                                    }
334                                    WalEntryType::Delete => {
335                                        state.store.remove(&buffered_entry.key);
336                                    }
337                                    _ => {}
338                                }
339                            }
340                            state.committed_count += 1;
341                        }
342                    }
343                    stats.entries_applied += 1;
344                    state.sequence = entry.sequence;
345                    stats.final_sequence = entry.sequence;
346                }
347                WalEntryType::Rollback => {
348                    if let Some(tx_id) = entry.transaction_id {
349                        state.active_transactions.remove(&tx_id);
350                        state.rolled_back_count += 1;
351                    }
352                    stats.entries_applied += 1;
353                    state.sequence = entry.sequence;
354                    stats.final_sequence = entry.sequence;
355                }
356                WalEntryType::Put => {
357                    if let Some(tx_id) = entry.transaction_id {
358                        state
359                            .active_transactions
360                            .entry(tx_id)
361                            .or_default()
362                            .push(entry.clone());
363                    } else if let Some(v) = &entry.value {
364                        state.store.insert(entry.key.clone(), v.clone());
365                    }
366                    stats.entries_applied += 1;
367                    state.sequence = entry.sequence;
368                    stats.final_sequence = entry.sequence;
369                }
370                WalEntryType::Delete => {
371                    if let Some(tx_id) = entry.transaction_id {
372                        state
373                            .active_transactions
374                            .entry(tx_id)
375                            .or_default()
376                            .push(entry.clone());
377                    } else {
378                        state.store.remove(&entry.key);
379                    }
380                    stats.entries_applied += 1;
381                    state.sequence = entry.sequence;
382                    stats.final_sequence = entry.sequence;
383                }
384            }
385        }
386
387        (state, stats)
388    }
389
390    // -----------------------------------------------------------------------
391    // verify_entries
392    // -----------------------------------------------------------------------
393
394    /// Verifies all entries in the log.
395    ///
396    /// Returns `(valid_count, invalid_count)`.
397    pub fn verify_entries(&self) -> (usize, usize) {
398        let mut valid = 0usize;
399        let mut invalid = 0usize;
400        for entry in &self.entries {
401            if entry.is_valid() {
402                valid += 1;
403            } else {
404                invalid += 1;
405            }
406        }
407        (valid, invalid)
408    }
409
410    // -----------------------------------------------------------------------
411    // last_checkpoint_sequence
412    // -----------------------------------------------------------------------
413
414    /// Returns the sequence number of the most recent Checkpoint entry, or `None`.
415    pub fn last_checkpoint_sequence(&self) -> Option<u64> {
416        self.checkpoint_sequences.last().copied()
417    }
418
419    // -----------------------------------------------------------------------
420    // truncate_before
421    // -----------------------------------------------------------------------
422
423    /// Removes all entries with sequence number strictly less than `sequence`.
424    ///
425    /// Returns the number of entries removed.
426    pub fn truncate_before(&mut self, sequence: u64) -> usize {
427        let before = self.entries.len();
428        self.entries.retain(|e| e.sequence >= sequence);
429        self.checkpoint_sequences.retain(|&s| s >= sequence);
430        before - self.entries.len()
431    }
432
433    // -----------------------------------------------------------------------
434    // wal_stats
435    // -----------------------------------------------------------------------
436
437    /// Returns aggregate statistics describing the current WAL buffer.
438    pub fn wal_stats(&self) -> WalStats {
439        let total_entries = self.entries.len();
440        let checkpoint_count = self
441            .entries
442            .iter()
443            .filter(|e| e.entry_type == WalEntryType::Checkpoint)
444            .count();
445
446        let oldest_sequence = self.entries.front().map_or(0, |e| e.sequence);
447        let newest_sequence = self.entries.back().map_or(0, |e| e.sequence);
448
449        let estimated_replay_size = self
450            .entries
451            .iter()
452            .map(|e| {
453                // Fixed overhead + key + value
454                32usize + e.key.len() + e.value.as_deref().map_or(0, |v| v.len())
455            })
456            .sum();
457
458        WalStats {
459            total_entries,
460            checkpoint_count,
461            oldest_sequence,
462            newest_sequence,
463            estimated_replay_size,
464        }
465    }
466
467    // -----------------------------------------------------------------------
468    // Accessor helpers
469    // -----------------------------------------------------------------------
470
471    /// Returns the number of entries currently buffered.
472    pub fn len(&self) -> usize {
473        self.entries.len()
474    }
475
476    /// Returns `true` if the log contains no entries.
477    pub fn is_empty(&self) -> bool {
478        self.entries.is_empty()
479    }
480
481    /// Returns the next sequence number that will be assigned.
482    pub fn next_sequence(&self) -> u64 {
483        self.next_sequence
484    }
485}
486
487// ---------------------------------------------------------------------------
488// Tests
489// ---------------------------------------------------------------------------
490
491#[cfg(test)]
492mod tests {
493    use super::{ReplayPolicy, StorageWALReplay, WalEntry, WalEntryType};
494
495    // -----------------------------------------------------------------------
496    // Helper builders
497    // -----------------------------------------------------------------------
498
499    fn make_replay(max: usize) -> StorageWALReplay {
500        StorageWALReplay::new(max)
501    }
502
503    fn put(log: &mut StorageWALReplay, key: &str, val: &[u8]) -> WalEntry {
504        log.append(WalEntryType::Put, key.to_string(), Some(val.to_vec()), None)
505    }
506
507    fn delete(log: &mut StorageWALReplay, key: &str) -> WalEntry {
508        log.append(WalEntryType::Delete, key.to_string(), None, None)
509    }
510
511    fn checkpoint(log: &mut StorageWALReplay) -> WalEntry {
512        log.append(WalEntryType::Checkpoint, String::new(), None, None)
513    }
514
515    fn begin(log: &mut StorageWALReplay, tx: u64) -> WalEntry {
516        log.append(WalEntryType::Begin, String::new(), None, Some(tx))
517    }
518
519    fn commit(log: &mut StorageWALReplay, tx: u64) -> WalEntry {
520        log.append(WalEntryType::Commit, String::new(), None, Some(tx))
521    }
522
523    fn rollback(log: &mut StorageWALReplay, tx: u64) -> WalEntry {
524        log.append(WalEntryType::Rollback, String::new(), None, Some(tx))
525    }
526
527    fn tx_put(log: &mut StorageWALReplay, key: &str, val: &[u8], tx: u64) -> WalEntry {
528        log.append(
529            WalEntryType::Put,
530            key.to_string(),
531            Some(val.to_vec()),
532            Some(tx),
533        )
534    }
535
536    fn tx_delete(log: &mut StorageWALReplay, key: &str, tx: u64) -> WalEntry {
537        log.append(WalEntryType::Delete, key.to_string(), None, Some(tx))
538    }
539
540    // -----------------------------------------------------------------------
541    // 1. Basic construction
542    // -----------------------------------------------------------------------
543
544    #[test]
545    fn test_new_is_empty() {
546        let log = make_replay(100);
547        assert!(log.is_empty());
548        assert_eq!(log.len(), 0);
549        assert_eq!(log.next_sequence(), 1);
550    }
551
552    // -----------------------------------------------------------------------
553    // 2. Sequence assignment
554    // -----------------------------------------------------------------------
555
556    #[test]
557    fn test_sequence_increments() {
558        let mut log = make_replay(100);
559        let e1 = put(&mut log, "a", b"1");
560        let e2 = put(&mut log, "b", b"2");
561        let e3 = put(&mut log, "c", b"3");
562        assert_eq!(e1.sequence, 1);
563        assert_eq!(e2.sequence, 2);
564        assert_eq!(e3.sequence, 3);
565        assert_eq!(log.next_sequence(), 4);
566    }
567
568    // -----------------------------------------------------------------------
569    // 3. Checksum validity
570    // -----------------------------------------------------------------------
571
572    #[test]
573    fn test_checksum_valid_on_append() {
574        let mut log = make_replay(100);
575        let entry = put(&mut log, "key", b"value");
576        assert!(entry.is_valid());
577    }
578
579    #[test]
580    fn test_checksum_detects_tampering() {
581        let mut entry = WalEntry {
582            sequence: 1,
583            entry_type: WalEntryType::Put,
584            key: "k".to_string(),
585            value: Some(b"v".to_vec()),
586            transaction_id: None,
587            checksum: WalEntry::compute_checksum(1, "k", &Some(b"v".to_vec())),
588        };
589        assert!(entry.is_valid());
590        // Tamper with the key.
591        entry.key = "tampered".to_string();
592        assert!(!entry.is_valid());
593    }
594
595    #[test]
596    fn test_checksum_value_none_vs_some() {
597        let cs_none = WalEntry::compute_checksum(1, "k", &None);
598        let cs_some = WalEntry::compute_checksum(1, "k", &Some(b"data".to_vec()));
599        assert_ne!(cs_none, cs_some);
600    }
601
602    // -----------------------------------------------------------------------
603    // 4. verify_entries
604    // -----------------------------------------------------------------------
605
606    #[test]
607    fn test_verify_all_valid() {
608        let mut log = make_replay(100);
609        put(&mut log, "a", b"1");
610        put(&mut log, "b", b"2");
611        let (valid, invalid) = log.verify_entries();
612        assert_eq!(valid, 2);
613        assert_eq!(invalid, 0);
614    }
615
616    // -----------------------------------------------------------------------
617    // 5. Full replay — basic Put
618    // -----------------------------------------------------------------------
619
620    #[test]
621    fn test_full_replay_puts() {
622        let mut log = make_replay(100);
623        put(&mut log, "x", b"hello");
624        put(&mut log, "y", b"world");
625        let (state, stats) = log.replay(ReplayPolicy::Full);
626        assert_eq!(
627            state.store.get("x").map(|v| v.as_slice()),
628            Some(b"hello".as_ref())
629        );
630        assert_eq!(
631            state.store.get("y").map(|v| v.as_slice()),
632            Some(b"world".as_ref())
633        );
634        assert_eq!(stats.entries_applied, 2);
635        assert_eq!(stats.entries_skipped, 0);
636        assert_eq!(stats.invalid_entries, 0);
637        assert_eq!(stats.final_sequence, 2);
638    }
639
640    // -----------------------------------------------------------------------
641    // 6. Full replay — Put then Delete
642    // -----------------------------------------------------------------------
643
644    #[test]
645    fn test_full_replay_delete_removes_key() {
646        let mut log = make_replay(100);
647        put(&mut log, "k", b"val");
648        delete(&mut log, "k");
649        let (state, stats) = log.replay(ReplayPolicy::Full);
650        assert!(!state.store.contains_key("k"));
651        assert_eq!(stats.entries_applied, 2);
652    }
653
654    // -----------------------------------------------------------------------
655    // 7. Full replay — overwrite key
656    // -----------------------------------------------------------------------
657
658    #[test]
659    fn test_full_replay_overwrite() {
660        let mut log = make_replay(100);
661        put(&mut log, "k", b"v1");
662        put(&mut log, "k", b"v2");
663        let (state, _) = log.replay(ReplayPolicy::Full);
664        assert_eq!(state.store["k"], b"v2");
665    }
666
667    // -----------------------------------------------------------------------
668    // 8. Checkpoint replay — SinceCheckpoint
669    // -----------------------------------------------------------------------
670
671    #[test]
672    fn test_since_checkpoint_skips_pre_checkpoint() {
673        let mut log = make_replay(100);
674        put(&mut log, "before", b"old");
675        checkpoint(&mut log);
676        put(&mut log, "after", b"new");
677        let (state, stats) = log.replay(ReplayPolicy::SinceCheckpoint);
678        // "before" was put before the checkpoint so it may be included since
679        // SinceCheckpoint starts AT the checkpoint entry (seq 2), not after it.
680        // The checkpoint itself is at seq=2, "before" is at seq=1 → skipped.
681        assert!(!state.store.contains_key("before"));
682        assert!(state.store.contains_key("after"));
683        assert_eq!(stats.entries_skipped, 1);
684        assert_eq!(stats.checkpoints_found, 1);
685    }
686
687    // -----------------------------------------------------------------------
688    // 9. SinceCheckpoint — no checkpoint falls back to empty replay
689    // -----------------------------------------------------------------------
690
691    #[test]
692    fn test_since_checkpoint_no_checkpoint_replays_all() {
693        let mut log = make_replay(100);
694        put(&mut log, "a", b"1");
695        put(&mut log, "b", b"2");
696        // No checkpoint → start_seq = 0, everything replayed.
697        let (state, stats) = log.replay(ReplayPolicy::SinceCheckpoint);
698        assert_eq!(state.store.len(), 2);
699        assert_eq!(stats.entries_skipped, 0);
700    }
701
702    // -----------------------------------------------------------------------
703    // 10. SinceSequence replay
704    // -----------------------------------------------------------------------
705
706    #[test]
707    fn test_since_sequence_skips_older() {
708        let mut log = make_replay(100);
709        put(&mut log, "a", b"1"); // seq 1
710        put(&mut log, "b", b"2"); // seq 2
711        put(&mut log, "c", b"3"); // seq 3
712        let (state, stats) = log.replay(ReplayPolicy::SinceSequence(2));
713        assert!(!state.store.contains_key("a"));
714        assert!(state.store.contains_key("b"));
715        assert!(state.store.contains_key("c"));
716        assert_eq!(stats.entries_skipped, 1);
717        assert_eq!(stats.entries_applied, 2);
718    }
719
720    // -----------------------------------------------------------------------
721    // 11. LastN replay
722    // -----------------------------------------------------------------------
723
724    #[test]
725    fn test_last_n_replay() {
726        let mut log = make_replay(100);
727        put(&mut log, "a", b"1");
728        put(&mut log, "b", b"2");
729        put(&mut log, "c", b"3");
730        put(&mut log, "d", b"4");
731        let (state, stats) = log.replay(ReplayPolicy::LastN(2));
732        // Only last 2: "c" and "d".
733        assert!(!state.store.contains_key("a"));
734        assert!(!state.store.contains_key("b"));
735        assert!(state.store.contains_key("c"));
736        assert!(state.store.contains_key("d"));
737        assert_eq!(stats.entries_skipped, 2);
738        assert_eq!(stats.entries_applied, 2);
739    }
740
741    #[test]
742    fn test_last_n_larger_than_log() {
743        let mut log = make_replay(100);
744        put(&mut log, "a", b"1");
745        put(&mut log, "b", b"2");
746        let (state, stats) = log.replay(ReplayPolicy::LastN(50));
747        assert_eq!(state.store.len(), 2);
748        assert_eq!(stats.entries_skipped, 0);
749        assert_eq!(stats.entries_applied, 2);
750    }
751
752    // -----------------------------------------------------------------------
753    // 12. Eviction when at capacity
754    // -----------------------------------------------------------------------
755
756    #[test]
757    fn test_eviction_at_capacity() {
758        let mut log = make_replay(3);
759        put(&mut log, "a", b"1"); // seq 1
760        put(&mut log, "b", b"2"); // seq 2
761        put(&mut log, "c", b"3"); // seq 3
762                                  // Now at capacity — next append evicts seq 1.
763        put(&mut log, "d", b"4"); // seq 4
764        assert_eq!(log.len(), 3);
765        let (state, _) = log.replay(ReplayPolicy::Full);
766        assert!(!state.store.contains_key("a"));
767        assert!(state.store.contains_key("b"));
768        assert!(state.store.contains_key("c"));
769        assert!(state.store.contains_key("d"));
770    }
771
772    // -----------------------------------------------------------------------
773    // 13. Checkpoint eviction
774    // -----------------------------------------------------------------------
775
776    #[test]
777    fn test_checkpoint_evicted_from_sequences() {
778        let mut log = make_replay(3);
779        checkpoint(&mut log); // seq 1 — evicted when seq 4 arrives
780        put(&mut log, "a", b"1");
781        put(&mut log, "b", b"2");
782        // seq 4 evicts seq 1 (the checkpoint).
783        put(&mut log, "c", b"3");
784        // The checkpoint at seq=1 should no longer be tracked.
785        assert!(log.last_checkpoint_sequence().is_none());
786    }
787
788    // -----------------------------------------------------------------------
789    // 14. last_checkpoint_sequence
790    // -----------------------------------------------------------------------
791
792    #[test]
793    fn test_last_checkpoint_sequence() {
794        let mut log = make_replay(100);
795        assert_eq!(log.last_checkpoint_sequence(), None);
796        checkpoint(&mut log); // seq 1
797        assert_eq!(log.last_checkpoint_sequence(), Some(1));
798        put(&mut log, "k", b"v");
799        checkpoint(&mut log); // seq 3
800        assert_eq!(log.last_checkpoint_sequence(), Some(3));
801    }
802
803    // -----------------------------------------------------------------------
804    // 15. truncate_before
805    // -----------------------------------------------------------------------
806
807    #[test]
808    fn test_truncate_before_removes_entries() {
809        let mut log = make_replay(100);
810        put(&mut log, "a", b"1"); // seq 1
811        put(&mut log, "b", b"2"); // seq 2
812        put(&mut log, "c", b"3"); // seq 3
813        let removed = log.truncate_before(2);
814        assert_eq!(removed, 1);
815        assert_eq!(log.len(), 2);
816    }
817
818    #[test]
819    fn test_truncate_before_removes_checkpoint_sequences() {
820        let mut log = make_replay(100);
821        checkpoint(&mut log); // seq 1
822        put(&mut log, "a", b"1");
823        checkpoint(&mut log); // seq 3
824        let removed = log.truncate_before(3);
825        assert_eq!(removed, 2); // seq 1 checkpoint + seq 2 put
826                                // Only checkpoint at seq 3 should remain.
827        assert_eq!(log.last_checkpoint_sequence(), Some(3));
828    }
829
830    #[test]
831    fn test_truncate_before_zero_removes_nothing() {
832        let mut log = make_replay(100);
833        put(&mut log, "a", b"1");
834        put(&mut log, "b", b"2");
835        let removed = log.truncate_before(0);
836        assert_eq!(removed, 0);
837        assert_eq!(log.len(), 2);
838    }
839
840    // -----------------------------------------------------------------------
841    // 16. wal_stats
842    // -----------------------------------------------------------------------
843
844    #[test]
845    fn test_wal_stats_empty() {
846        let log = make_replay(100);
847        let s = log.wal_stats();
848        assert_eq!(s.total_entries, 0);
849        assert_eq!(s.checkpoint_count, 0);
850        assert_eq!(s.oldest_sequence, 0);
851        assert_eq!(s.newest_sequence, 0);
852        assert_eq!(s.estimated_replay_size, 0);
853    }
854
855    #[test]
856    fn test_wal_stats_with_entries() {
857        let mut log = make_replay(100);
858        put(&mut log, "a", b"hello");
859        checkpoint(&mut log);
860        put(&mut log, "b", b"world");
861        let s = log.wal_stats();
862        assert_eq!(s.total_entries, 3);
863        assert_eq!(s.checkpoint_count, 1);
864        assert_eq!(s.oldest_sequence, 1);
865        assert_eq!(s.newest_sequence, 3);
866        assert!(s.estimated_replay_size > 0);
867    }
868
869    // -----------------------------------------------------------------------
870    // 17. Transaction: Begin + Put + Commit
871    // -----------------------------------------------------------------------
872
873    #[test]
874    fn test_transaction_commit_applies_ops() {
875        let mut log = make_replay(100);
876        begin(&mut log, 42);
877        tx_put(&mut log, "k", b"value", 42);
878        commit(&mut log, 42);
879        let (state, stats) = log.replay(ReplayPolicy::Full);
880        assert_eq!(
881            state.store.get("k").map(|v| v.as_slice()),
882            Some(b"value".as_ref())
883        );
884        assert_eq!(state.committed_count, 1);
885        assert!(state.active_transactions.is_empty());
886        assert_eq!(stats.entries_applied, 3);
887    }
888
889    // -----------------------------------------------------------------------
890    // 18. Transaction: Begin + Put + Rollback
891    // -----------------------------------------------------------------------
892
893    #[test]
894    fn test_transaction_rollback_discards_ops() {
895        let mut log = make_replay(100);
896        begin(&mut log, 7);
897        tx_put(&mut log, "k", b"should_not_appear", 7);
898        rollback(&mut log, 7);
899        let (state, stats) = log.replay(ReplayPolicy::Full);
900        assert!(!state.store.contains_key("k"));
901        assert_eq!(state.rolled_back_count, 1);
902        assert_eq!(stats.entries_applied, 3);
903    }
904
905    // -----------------------------------------------------------------------
906    // 19. Transaction: Delete buffered and committed
907    // -----------------------------------------------------------------------
908
909    #[test]
910    fn test_transaction_commit_delete() {
911        let mut log = make_replay(100);
912        // Pre-populate outside any tx.
913        put(&mut log, "k", b"v");
914        begin(&mut log, 1);
915        tx_delete(&mut log, "k", 1);
916        commit(&mut log, 1);
917        let (state, _) = log.replay(ReplayPolicy::Full);
918        assert!(!state.store.contains_key("k"));
919    }
920
921    // -----------------------------------------------------------------------
922    // 20. Multiple concurrent transactions
923    // -----------------------------------------------------------------------
924
925    #[test]
926    fn test_two_concurrent_transactions() {
927        let mut log = make_replay(100);
928        begin(&mut log, 1);
929        begin(&mut log, 2);
930        tx_put(&mut log, "a", b"from-tx1", 1);
931        tx_put(&mut log, "b", b"from-tx2", 2);
932        commit(&mut log, 1);
933        rollback(&mut log, 2);
934        let (state, _) = log.replay(ReplayPolicy::Full);
935        assert_eq!(
936            state.store.get("a").map(|v| v.as_slice()),
937            Some(b"from-tx1".as_ref())
938        );
939        assert!(!state.store.contains_key("b"));
940        assert_eq!(state.committed_count, 1);
941        assert_eq!(state.rolled_back_count, 1);
942    }
943
944    // -----------------------------------------------------------------------
945    // 21. Transaction left open (no Commit/Rollback)
946    // -----------------------------------------------------------------------
947
948    #[test]
949    fn test_open_transaction_remains_in_active() {
950        let mut log = make_replay(100);
951        begin(&mut log, 99);
952        tx_put(&mut log, "k", b"v", 99);
953        // No commit or rollback.
954        let (state, _) = log.replay(ReplayPolicy::Full);
955        assert!(!state.store.contains_key("k"));
956        assert!(state.active_transactions.contains_key(&99));
957    }
958
959    // -----------------------------------------------------------------------
960    // 22. Invalid entry skipped in stats
961    // -----------------------------------------------------------------------
962
963    #[test]
964    fn test_invalid_entry_counted_not_applied() {
965        let mut log = make_replay(100);
966        // Append a valid entry.
967        put(&mut log, "valid_key", b"v");
968        // Manually push a corrupt entry.
969        log.entries.push_back(WalEntry {
970            sequence: 999,
971            entry_type: WalEntryType::Put,
972            key: "bad_key".to_string(),
973            value: Some(b"data".to_vec()),
974            transaction_id: None,
975            checksum: 0xDEAD_BEEF, // deliberately wrong
976        });
977        let (state, stats) = log.replay(ReplayPolicy::Full);
978        assert_eq!(stats.invalid_entries, 1);
979        assert!(!state.store.contains_key("bad_key"));
980        assert!(state.store.contains_key("valid_key"));
981    }
982
983    // -----------------------------------------------------------------------
984    // 23. Empty log replay
985    // -----------------------------------------------------------------------
986
987    #[test]
988    fn test_replay_empty_log() {
989        let log = make_replay(100);
990        let (state, stats) = log.replay(ReplayPolicy::Full);
991        assert!(state.store.is_empty());
992        assert_eq!(stats.entries_read, 0);
993        assert_eq!(stats.entries_applied, 0);
994        assert_eq!(stats.final_sequence, 0);
995    }
996
997    // -----------------------------------------------------------------------
998    // 24. SinceSequence with exact boundary
999    // -----------------------------------------------------------------------
1000
1001    #[test]
1002    fn test_since_sequence_exact_boundary() {
1003        let mut log = make_replay(100);
1004        put(&mut log, "a", b"1"); // seq 1
1005        put(&mut log, "b", b"2"); // seq 2
1006                                  // SinceSequence(1) should include everything.
1007        let (state, stats) = log.replay(ReplayPolicy::SinceSequence(1));
1008        assert_eq!(state.store.len(), 2);
1009        assert_eq!(stats.entries_skipped, 0);
1010    }
1011
1012    // -----------------------------------------------------------------------
1013    // 25. Checkpoint count in stats
1014    // -----------------------------------------------------------------------
1015
1016    #[test]
1017    fn test_replay_stats_checkpoint_count() {
1018        let mut log = make_replay(100);
1019        checkpoint(&mut log);
1020        put(&mut log, "a", b"1");
1021        checkpoint(&mut log);
1022        let (_, stats) = log.replay(ReplayPolicy::Full);
1023        assert_eq!(stats.checkpoints_found, 2);
1024    }
1025
1026    // -----------------------------------------------------------------------
1027    // 26. wal_stats estimated_replay_size
1028    // -----------------------------------------------------------------------
1029
1030    #[test]
1031    fn test_wal_stats_estimated_size_increases_with_entries() {
1032        let mut log = make_replay(100);
1033        let s0 = log.wal_stats().estimated_replay_size;
1034        put(&mut log, "key", b"some_value");
1035        let s1 = log.wal_stats().estimated_replay_size;
1036        assert!(s1 > s0);
1037    }
1038
1039    // -----------------------------------------------------------------------
1040    // 27. Replay SinceCheckpoint — multiple checkpoints uses last one
1041    // -----------------------------------------------------------------------
1042
1043    #[test]
1044    fn test_since_checkpoint_uses_latest() {
1045        let mut log = make_replay(100);
1046        put(&mut log, "early", b"e"); // seq 1
1047        checkpoint(&mut log); // seq 2
1048        put(&mut log, "mid", b"m"); // seq 3
1049        checkpoint(&mut log); // seq 4
1050        put(&mut log, "late", b"l"); // seq 5
1051        let (state, stats) = log.replay(ReplayPolicy::SinceCheckpoint);
1052        // Should start from seq 4 (last checkpoint).
1053        assert!(!state.store.contains_key("early"));
1054        assert!(!state.store.contains_key("mid"));
1055        assert!(state.store.contains_key("late"));
1056        assert_eq!(stats.checkpoints_found, 1); // only the last checkpoint in window
1057    }
1058
1059    // -----------------------------------------------------------------------
1060    // 28. LastN(0) replays nothing
1061    // -----------------------------------------------------------------------
1062
1063    #[test]
1064    fn test_last_n_zero() {
1065        let mut log = make_replay(100);
1066        put(&mut log, "a", b"1");
1067        put(&mut log, "b", b"2");
1068        let (state, stats) = log.replay(ReplayPolicy::LastN(0));
1069        assert!(state.store.is_empty());
1070        assert_eq!(stats.entries_applied, 0);
1071        assert_eq!(stats.entries_skipped, 2);
1072    }
1073
1074    // -----------------------------------------------------------------------
1075    // 29. Truncate then replay
1076    // -----------------------------------------------------------------------
1077
1078    #[test]
1079    fn test_truncate_then_replay() {
1080        let mut log = make_replay(100);
1081        put(&mut log, "a", b"1"); // seq 1
1082        put(&mut log, "b", b"2"); // seq 2
1083        put(&mut log, "c", b"3"); // seq 3
1084        log.truncate_before(3);
1085        let (state, stats) = log.replay(ReplayPolicy::Full);
1086        assert!(!state.store.contains_key("a"));
1087        assert!(!state.store.contains_key("b"));
1088        assert!(state.store.contains_key("c"));
1089        assert_eq!(stats.entries_applied, 1);
1090    }
1091
1092    // -----------------------------------------------------------------------
1093    // 30. ReplayState sequence tracks last applied entry
1094    // -----------------------------------------------------------------------
1095
1096    #[test]
1097    fn test_replay_state_sequence_final() {
1098        let mut log = make_replay(100);
1099        put(&mut log, "a", b"1");
1100        put(&mut log, "b", b"2");
1101        put(&mut log, "c", b"3");
1102        let (state, stats) = log.replay(ReplayPolicy::Full);
1103        assert_eq!(state.sequence, 3);
1104        assert_eq!(stats.final_sequence, 3);
1105    }
1106
1107    // -----------------------------------------------------------------------
1108    // 31. Checkpoint entry type round-trips through append
1109    // -----------------------------------------------------------------------
1110
1111    #[test]
1112    fn test_checkpoint_entry_is_valid() {
1113        let mut log = make_replay(100);
1114        let e = checkpoint(&mut log);
1115        assert_eq!(e.entry_type, WalEntryType::Checkpoint);
1116        assert!(e.is_valid());
1117    }
1118
1119    // -----------------------------------------------------------------------
1120    // 32. Multiple Puts to same key, only latest survives
1121    // -----------------------------------------------------------------------
1122
1123    #[test]
1124    fn test_multiple_puts_last_wins() {
1125        let mut log = make_replay(100);
1126        for i in 0u8..10 {
1127            let val = vec![i];
1128            put(&mut log, "k", &val);
1129        }
1130        let (state, _) = log.replay(ReplayPolicy::Full);
1131        assert_eq!(state.store["k"], vec![9u8]);
1132    }
1133
1134    // -----------------------------------------------------------------------
1135    // 33. Transaction with multiple Puts and Deletes
1136    // -----------------------------------------------------------------------
1137
1138    #[test]
1139    fn test_transaction_multiple_ops_committed() {
1140        let mut log = make_replay(100);
1141        // Pre-populate some keys outside tx.
1142        put(&mut log, "keep", b"k");
1143        put(&mut log, "remove", b"r");
1144        begin(&mut log, 1);
1145        tx_put(&mut log, "new_key", b"n", 1);
1146        tx_delete(&mut log, "remove", 1);
1147        commit(&mut log, 1);
1148        let (state, _) = log.replay(ReplayPolicy::Full);
1149        assert!(state.store.contains_key("keep"));
1150        assert!(!state.store.contains_key("remove"));
1151        assert!(state.store.contains_key("new_key"));
1152    }
1153
1154    // -----------------------------------------------------------------------
1155    // 34. WalStats with max_entries respected
1156    // -----------------------------------------------------------------------
1157
1158    #[test]
1159    fn test_wal_stats_after_eviction() {
1160        let mut log = make_replay(5);
1161        for i in 0u8..10 {
1162            put(&mut log, "k", &[i]);
1163        }
1164        let s = log.wal_stats();
1165        assert_eq!(s.total_entries, 5);
1166        // After eviction, oldest seq should be 6.
1167        assert_eq!(s.oldest_sequence, 6);
1168        assert_eq!(s.newest_sequence, 10);
1169    }
1170
1171    // -----------------------------------------------------------------------
1172    // 35. compute_checksum is deterministic
1173    // -----------------------------------------------------------------------
1174
1175    #[test]
1176    fn test_checksum_deterministic() {
1177        let v = Some(b"data".to_vec());
1178        let c1 = WalEntry::compute_checksum(42, "mykey", &v);
1179        let c2 = WalEntry::compute_checksum(42, "mykey", &v);
1180        assert_eq!(c1, c2);
1181    }
1182
1183    // -----------------------------------------------------------------------
1184    // 36. Delete non-existent key is a no-op
1185    // -----------------------------------------------------------------------
1186
1187    #[test]
1188    fn test_delete_nonexistent_key_noop() {
1189        let mut log = make_replay(100);
1190        delete(&mut log, "ghost");
1191        let (state, stats) = log.replay(ReplayPolicy::Full);
1192        assert!(state.store.is_empty());
1193        assert_eq!(stats.entries_applied, 1);
1194    }
1195
1196    // -----------------------------------------------------------------------
1197    // 37. SinceSequence beyond newest replays nothing
1198    // -----------------------------------------------------------------------
1199
1200    #[test]
1201    fn test_since_sequence_beyond_newest() {
1202        let mut log = make_replay(100);
1203        put(&mut log, "a", b"1");
1204        put(&mut log, "b", b"2");
1205        let (state, stats) = log.replay(ReplayPolicy::SinceSequence(999));
1206        assert!(state.store.is_empty());
1207        assert_eq!(stats.entries_applied, 0);
1208        assert_eq!(stats.entries_skipped, 2);
1209    }
1210}