ipfrs_storage/snapshot_manager.rs
1//! `StorageSnapshotManager` — Point-in-time snapshot management for storage
2//! state with incremental deltas and restoration.
3//!
4//! # Overview
5//!
6//! This module provides two distinct APIs:
7//!
8//! 1. **New production-grade API** (`StorageSnapshotManager`, `SnapshotId`,
9//! `SnapshotEntry`, `SnapshotDelta`, `SsmSnapshot`, `StorageState`,
10//! `SnapshotError`, `SnapshotStats`) — full point-in-time snapshot
11//! management with FNV-1a checksums, incremental deltas, and chain-based
12//! restoration.
13//!
14//! 2. **Legacy block-snapshot API** (`LegacySnapshotEntry`, `fnv1a_64`,
15//! `LegacySnapshot`, `SnapshotKind`, `StorageSnapshot`, `SnapshotConfig`,
16//! `SnapshotManagerStats`) — retained for backwards compatibility.
17
18use std::collections::{HashMap, VecDeque};
19use thiserror::Error;
20
21// ---------------------------------------------------------------------------
22// SnapshotId
23// ---------------------------------------------------------------------------
24
25/// Monotonically increasing snapshot identifier.
26#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
27pub struct SnapshotId(pub u64);
28
29impl std::fmt::Display for SnapshotId {
30 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31 write!(f, "SnapshotId({})", self.0)
32 }
33}
34
35// ---------------------------------------------------------------------------
36// SnapshotEntry
37// ---------------------------------------------------------------------------
38
39/// A key-value record at snapshot time.
40#[derive(Clone, Debug, PartialEq, Eq)]
41pub struct SnapshotEntry {
42 /// The storage key.
43 pub key: String,
44 /// The raw value bytes.
45 pub value: Vec<u8>,
46 /// Logical version counter, incremented on each write to the same key.
47 pub version: u64,
48}
49
50// ---------------------------------------------------------------------------
51// SnapshotDelta
52// ---------------------------------------------------------------------------
53
54/// Changes relative to the previous snapshot.
55#[derive(Clone, Debug, Default, PartialEq, Eq)]
56pub struct SnapshotDelta {
57 /// Entries that were added since the previous snapshot.
58 pub added: Vec<SnapshotEntry>,
59 /// Entries that were modified since the previous snapshot.
60 pub modified: Vec<SnapshotEntry>,
61 /// Keys that were deleted since the previous snapshot.
62 pub deleted: Vec<String>,
63}
64
65impl SnapshotDelta {
66 /// Returns `true` if the delta contains no changes at all.
67 pub fn is_empty(&self) -> bool {
68 self.added.is_empty() && self.modified.is_empty() && self.deleted.is_empty()
69 }
70}
71
72// ---------------------------------------------------------------------------
73// SsmSnapshot (new Snapshot type)
74// ---------------------------------------------------------------------------
75
76/// An immutable point-in-time snapshot of the storage state.
77#[derive(Clone, Debug)]
78pub struct SsmSnapshot {
79 /// Unique monotonically-increasing identifier.
80 pub id: SnapshotId,
81 /// Unix timestamp (or logical clock) at which the snapshot was taken.
82 pub created_at: u64,
83 /// Number of live entries captured.
84 pub entry_count: usize,
85 /// Sum of all value byte lengths at snapshot time.
86 pub total_bytes: u64,
87 /// `None` for the first (full) snapshot; `Some` for incremental snapshots.
88 pub delta: Option<SnapshotDelta>,
89 /// FNV-1a checksum computed over all key+value pairs sorted by key.
90 pub checksum: u64,
91 /// Optional human-readable label.
92 pub label: Option<String>,
93 /// The full reconstructed state at this snapshot point (used for chain
94 /// restoration without having to replay from the very beginning each time).
95 ///
96 /// Stored inline so that `restore_snapshot` is O(1) after chain walk.
97 pub(crate) state_at_snapshot: HashMap<String, SnapshotEntry>,
98}
99
100// ---------------------------------------------------------------------------
101// StorageState
102// ---------------------------------------------------------------------------
103
104/// The current live mutable storage state.
105#[derive(Clone, Debug, Default)]
106pub struct StorageState {
107 /// Active key-value entries.
108 pub entries: HashMap<String, SnapshotEntry>,
109}
110
111// ---------------------------------------------------------------------------
112// SnapshotError
113// ---------------------------------------------------------------------------
114
115/// Errors returned by `StorageSnapshotManager` operations.
116#[derive(Debug, Error, PartialEq, Eq)]
117pub enum SnapshotError {
118 /// The requested snapshot id was not found.
119 #[error("snapshot not found: {0}")]
120 SnapshotNotFound(u64),
121
122 /// Attempted to delete a snapshot that is not the oldest.
123 #[error("only the oldest snapshot may be deleted")]
124 CannotDeleteNonOldest,
125
126 /// Operation requires at least one snapshot but none exist.
127 #[error("snapshot chain is empty")]
128 EmptySnapshotChain,
129
130 /// The delta chain is broken (an incremental snapshot references a missing
131 /// predecessor).
132 #[error("delta chain is broken")]
133 DeltaChainBroken,
134}
135
136// ---------------------------------------------------------------------------
137// SnapshotStats
138// ---------------------------------------------------------------------------
139
140/// Aggregate statistics for `StorageSnapshotManager`.
141#[derive(Clone, Debug, Default)]
142pub struct SnapshotStats {
143 /// Total number of snapshots retained.
144 pub snapshot_count: usize,
145 /// Id of the oldest retained snapshot, if any.
146 pub oldest_snapshot_id: Option<u64>,
147 /// Id of the newest retained snapshot, if any.
148 pub newest_snapshot_id: Option<u64>,
149 /// Sum of `total_bytes` across all retained snapshots.
150 pub total_snapshot_bytes: u64,
151 /// Number of entries in the current live state.
152 pub live_entries: usize,
153 /// Sum of value sizes in the current live state.
154 pub live_bytes: u64,
155}
156
157// ---------------------------------------------------------------------------
158// StorageSnapshotManager
159// ---------------------------------------------------------------------------
160
161/// Production-grade point-in-time snapshot manager with incremental deltas
162/// and chain-based restoration.
163pub struct StorageSnapshotManager {
164 /// The current live mutable state.
165 state: StorageState,
166 /// Ordered queue of retained snapshots (oldest first).
167 snapshots: VecDeque<SsmSnapshot>,
168 /// Maximum number of snapshots to retain before evicting the oldest.
169 max_snapshots: usize,
170 /// Next snapshot id to assign.
171 next_id: u64,
172 /// Next version counter to assign when writing entries.
173 next_version: u64,
174}
175
176// ---------------------------------------------------------------------------
177// Internal FNV-1a checksum helper
178// ---------------------------------------------------------------------------
179
180/// Compute an FNV-1a 64-bit checksum over all entries sorted by key.
181fn compute_checksum(entries: &HashMap<String, SnapshotEntry>) -> u64 {
182 let mut keys: Vec<&str> = entries.keys().map(|s| s.as_str()).collect();
183 keys.sort_unstable();
184 let mut h: u64 = 14_695_981_039_346_656_037;
185 for k in keys {
186 for b in k.bytes() {
187 h ^= u64::from(b);
188 h = h.wrapping_mul(1_099_511_628_211);
189 }
190 if let Some(e) = entries.get(k) {
191 for b in &e.value {
192 h ^= u64::from(*b);
193 h = h.wrapping_mul(1_099_511_628_211);
194 }
195 }
196 }
197 h
198}
199
200/// Reconstruct a `HashMap<String, SnapshotEntry>` state from a snapshot.
201/// For full snapshots (no delta), returns the stored state directly.
202/// For incremental snapshots, the state was already pre-computed and stored.
203fn reconstructed_state(snap: &SsmSnapshot) -> &HashMap<String, SnapshotEntry> {
204 &snap.state_at_snapshot
205}
206
207impl StorageSnapshotManager {
208 // -----------------------------------------------------------------------
209 // Construction
210 // -----------------------------------------------------------------------
211
212 /// Create a new, empty manager.
213 ///
214 /// `max_snapshots` controls how many snapshots are retained before the
215 /// oldest is evicted to make room for a new one.
216 pub fn new(max_snapshots: usize) -> Self {
217 Self {
218 state: StorageState::default(),
219 snapshots: VecDeque::new(),
220 max_snapshots: max_snapshots.max(1),
221 next_id: 1,
222 next_version: 1,
223 }
224 }
225
226 // -----------------------------------------------------------------------
227 // Live-state mutations
228 // -----------------------------------------------------------------------
229
230 /// Insert or update a key in the live state.
231 ///
232 /// The entry's `version` is set to `self.next_version` (then incremented).
233 pub fn put(&mut self, key: String, value: Vec<u8>, _now: u64) {
234 let version = self.next_version;
235 self.next_version += 1;
236 self.state.entries.insert(
237 key.clone(),
238 SnapshotEntry {
239 key,
240 value,
241 version,
242 },
243 );
244 }
245
246 /// Remove a key from the live state.
247 ///
248 /// Returns `false` if the key was not present.
249 pub fn delete(&mut self, key: &str) -> bool {
250 self.state.entries.remove(key).is_some()
251 }
252
253 /// Look up the value bytes for a key in the live state.
254 pub fn get(&self, key: &str) -> Option<&[u8]> {
255 self.state.entries.get(key).map(|e| e.value.as_slice())
256 }
257
258 // -----------------------------------------------------------------------
259 // Snapshot creation
260 // -----------------------------------------------------------------------
261
262 /// Capture the current live state as a new snapshot.
263 ///
264 /// - If this is the first snapshot, it is a *full* snapshot (`delta` is
265 /// `None`).
266 /// - Otherwise, a delta relative to the preceding snapshot is computed.
267 ///
268 /// If the queue is already at `max_snapshots`, the oldest snapshot is
269 /// evicted before appending the new one.
270 pub fn take_snapshot(&mut self, label: Option<String>, now: u64) -> SnapshotId {
271 let id = SnapshotId(self.next_id);
272 self.next_id += 1;
273
274 // Clone the current live state for the snapshot.
275 let current_state = self.state.entries.clone();
276
277 // Compute delta vs. the previous snapshot (if any).
278 let delta: Option<SnapshotDelta> = if let Some(prev) = self.snapshots.back() {
279 let prev_state = reconstructed_state(prev);
280 Some(compute_delta(prev_state, ¤t_state))
281 } else {
282 None
283 };
284
285 let checksum = compute_checksum(¤t_state);
286 let entry_count = current_state.len();
287 let total_bytes: u64 = current_state.values().map(|e| e.value.len() as u64).sum();
288
289 let snap = SsmSnapshot {
290 id,
291 created_at: now,
292 entry_count,
293 total_bytes,
294 delta,
295 checksum,
296 label,
297 state_at_snapshot: current_state,
298 };
299
300 // Evict oldest if at capacity.
301 if self.snapshots.len() >= self.max_snapshots {
302 self.snapshots.pop_front();
303 }
304
305 self.snapshots.push_back(snap);
306 id
307 }
308
309 // -----------------------------------------------------------------------
310 // Snapshot restoration
311 // -----------------------------------------------------------------------
312
313 /// Restore the live state to the point captured by snapshot `id`.
314 ///
315 /// The state stored inline in the snapshot is used directly (O(1) after
316 /// locating the snapshot in the deque).
317 pub fn restore_snapshot(&mut self, id: SnapshotId) -> Result<(), SnapshotError> {
318 let snap = self
319 .snapshots
320 .iter()
321 .find(|s| s.id == id)
322 .ok_or(SnapshotError::SnapshotNotFound(id.0))?;
323
324 self.state.entries = snap.state_at_snapshot.clone();
325 Ok(())
326 }
327
328 // -----------------------------------------------------------------------
329 // Snapshot queries
330 // -----------------------------------------------------------------------
331
332 /// Return all retained snapshots ordered oldest to newest.
333 pub fn list_snapshots(&self) -> Vec<&SsmSnapshot> {
334 self.snapshots.iter().collect()
335 }
336
337 /// Look up a snapshot by id.
338 pub fn get_snapshot(&self, id: SnapshotId) -> Option<&SsmSnapshot> {
339 self.snapshots.iter().find(|s| s.id == id)
340 }
341
342 /// Remove the oldest snapshot from the queue.
343 ///
344 /// Returns `CannotDeleteNonOldest` if `id` does not refer to the oldest
345 /// snapshot, and `SnapshotNotFound` if no snapshot with `id` exists.
346 pub fn delete_snapshot(&mut self, id: SnapshotId) -> Result<(), SnapshotError> {
347 // Must exist.
348 let exists = self.snapshots.iter().any(|s| s.id == id);
349 if !exists {
350 return Err(SnapshotError::SnapshotNotFound(id.0));
351 }
352
353 // Only allow deleting the oldest.
354 let oldest_id = self
355 .snapshots
356 .front()
357 .map(|s| s.id)
358 .ok_or(SnapshotError::EmptySnapshotChain)?;
359
360 if oldest_id != id {
361 return Err(SnapshotError::CannotDeleteNonOldest);
362 }
363
364 self.snapshots.pop_front();
365 Ok(())
366 }
367
368 /// Compute the full state diff between two snapshots.
369 ///
370 /// Reconstructs both states from the stored inline state, then compares.
371 pub fn diff_snapshots(
372 &self,
373 a: SnapshotId,
374 b: SnapshotId,
375 ) -> Result<SnapshotDelta, SnapshotError> {
376 let snap_a = self
377 .snapshots
378 .iter()
379 .find(|s| s.id == a)
380 .ok_or(SnapshotError::SnapshotNotFound(a.0))?;
381 let snap_b = self
382 .snapshots
383 .iter()
384 .find(|s| s.id == b)
385 .ok_or(SnapshotError::SnapshotNotFound(b.0))?;
386
387 let state_a = reconstructed_state(snap_a);
388 let state_b = reconstructed_state(snap_b);
389
390 Ok(compute_delta(state_a, state_b))
391 }
392
393 // -----------------------------------------------------------------------
394 // Counts and statistics
395 // -----------------------------------------------------------------------
396
397 /// Number of snapshots currently retained.
398 pub fn snapshot_count(&self) -> usize {
399 self.snapshots.len()
400 }
401
402 /// Number of entries in the current live state.
403 pub fn live_entry_count(&self) -> usize {
404 self.state.entries.len()
405 }
406
407 /// Sum of value byte sizes in the current live state.
408 pub fn live_total_bytes(&self) -> u64 {
409 self.state
410 .entries
411 .values()
412 .map(|e| e.value.len() as u64)
413 .sum()
414 }
415
416 /// Return aggregate statistics.
417 pub fn stats(&self) -> SnapshotStats {
418 let oldest_snapshot_id = self.snapshots.front().map(|s| s.id.0);
419 let newest_snapshot_id = self.snapshots.back().map(|s| s.id.0);
420 let total_snapshot_bytes: u64 = self.snapshots.iter().map(|s| s.total_bytes).sum();
421 SnapshotStats {
422 snapshot_count: self.snapshots.len(),
423 oldest_snapshot_id,
424 newest_snapshot_id,
425 total_snapshot_bytes,
426 live_entries: self.live_entry_count(),
427 live_bytes: self.live_total_bytes(),
428 }
429 }
430}
431
432// ---------------------------------------------------------------------------
433// Internal delta helper
434// ---------------------------------------------------------------------------
435
436/// Compute a `SnapshotDelta` describing changes from `prev` to `curr`.
437fn compute_delta(
438 prev: &HashMap<String, SnapshotEntry>,
439 curr: &HashMap<String, SnapshotEntry>,
440) -> SnapshotDelta {
441 let mut added = Vec::new();
442 let mut modified = Vec::new();
443 let mut deleted = Vec::new();
444
445 // Keys in curr that are new or changed.
446 for (key, curr_entry) in curr {
447 match prev.get(key) {
448 None => added.push(curr_entry.clone()),
449 Some(prev_entry) => {
450 if prev_entry.version != curr_entry.version || prev_entry.value != curr_entry.value
451 {
452 modified.push(curr_entry.clone());
453 }
454 }
455 }
456 }
457
458 // Keys in prev that are absent in curr.
459 for key in prev.keys() {
460 if !curr.contains_key(key) {
461 deleted.push(key.clone());
462 }
463 }
464
465 SnapshotDelta {
466 added,
467 modified,
468 deleted,
469 }
470}
471
472// ===========================================================================
473// Legacy block-snapshot API — kept for backwards compatibility
474// ===========================================================================
475
476// ---------------------------------------------------------------------------
477// SnapshotKind
478// ---------------------------------------------------------------------------
479
480/// Describes which blocks are recorded in a legacy snapshot.
481#[derive(Clone, Copy, Debug, PartialEq, Eq)]
482pub enum SnapshotKind {
483 /// A complete snapshot of every known block.
484 Full,
485 /// Only the blocks that changed since the immediately preceding snapshot.
486 Incremental,
487 /// All blocks that changed since the most recent `Full` snapshot.
488 Differential,
489}
490
491// ---------------------------------------------------------------------------
492// LegacySnapshotEntry
493// ---------------------------------------------------------------------------
494
495/// A single content-addressed block reference stored inside a legacy snapshot.
496#[derive(Clone, Debug, PartialEq)]
497pub struct LegacySnapshotEntry {
498 /// Content identifier of the block (e.g. a CIDv1 string).
499 pub cid: String,
500 /// Raw byte size of the block.
501 pub size_bytes: u64,
502 /// FNV-1a (64-bit) hash of `cid`, used for fast deduplication lookups.
503 pub hash: u64,
504}
505
506impl LegacySnapshotEntry {
507 /// Construct a new entry, computing the FNV-1a hash automatically.
508 pub fn new(cid: impl Into<String>, size_bytes: u64) -> Self {
509 let cid = cid.into();
510 let hash = fnv1a_64(cid.as_bytes());
511 Self {
512 cid,
513 size_bytes,
514 hash,
515 }
516 }
517}
518
519// ---------------------------------------------------------------------------
520// FNV-1a helpers (public)
521// ---------------------------------------------------------------------------
522
523/// FNV-1a 64-bit hash of an arbitrary byte slice.
524#[inline]
525pub fn fnv1a_64(data: &[u8]) -> u64 {
526 const OFFSET_BASIS: u64 = 14_695_981_039_346_656_037;
527 const PRIME: u64 = 1_099_511_628_211;
528 let mut hash = OFFSET_BASIS;
529 for &byte in data {
530 hash ^= u64::from(byte);
531 hash = hash.wrapping_mul(PRIME);
532 }
533 hash
534}
535
536// ---------------------------------------------------------------------------
537// LegacySnapshot
538// ---------------------------------------------------------------------------
539
540/// An immutable point-in-time view of a set of storage blocks (legacy API).
541#[derive(Clone, Debug)]
542pub struct LegacySnapshot {
543 /// Unique, monotonically-increasing identifier assigned by the manager.
544 pub snapshot_id: u64,
545 /// What subset of blocks this snapshot captures.
546 pub kind: SnapshotKind,
547 /// Unix timestamp (seconds since epoch) at which this snapshot was taken.
548 pub created_at_secs: u64,
549 /// The block entries recorded in this snapshot.
550 pub entries: Vec<LegacySnapshotEntry>,
551 /// For `Incremental` and `Differential` snapshots, the id of the parent
552 /// snapshot this one is relative to. Always `None` for `Full` snapshots.
553 pub parent_id: Option<u64>,
554}
555
556impl LegacySnapshot {
557 /// Sum of `size_bytes` across all entries.
558 pub fn total_size(&self) -> u64 {
559 self.entries.iter().map(|e| e.size_bytes).sum()
560 }
561
562 /// Number of block entries in this snapshot.
563 pub fn entry_count(&self) -> usize {
564 self.entries.len()
565 }
566}
567
568// ---------------------------------------------------------------------------
569// SnapshotState
570// ---------------------------------------------------------------------------
571
572/// Lifecycle state of a managed storage snapshot.
573#[derive(Debug, Clone, Copy, PartialEq, Eq)]
574pub enum SnapshotState {
575 /// Snapshot is being assembled (not yet queryable).
576 Creating,
577 /// Snapshot is complete and ready for restore / diff.
578 Ready,
579 /// Snapshot exceeded its TTL and is no longer restorable.
580 Expired,
581 /// Snapshot has been explicitly deleted by the user.
582 Deleted,
583}
584
585// ---------------------------------------------------------------------------
586// StorageSnapshot
587// ---------------------------------------------------------------------------
588
589/// A managed, stateful snapshot of storage blocks.
590#[derive(Debug, Clone)]
591pub struct StorageSnapshot {
592 /// Unique id assigned by the manager.
593 pub id: u64,
594 /// Human-readable label for this snapshot.
595 pub label: String,
596 /// Current lifecycle state.
597 pub state: SnapshotState,
598 /// Tick at which this snapshot was created.
599 pub created_tick: u64,
600 /// Number of blocks in this snapshot.
601 pub block_count: usize,
602 /// Total byte size of all blocks.
603 pub total_bytes: u64,
604 /// CIDs of the blocks captured in this snapshot.
605 pub block_cids: Vec<String>,
606}
607
608// ---------------------------------------------------------------------------
609// SnapshotConfig
610// ---------------------------------------------------------------------------
611
612/// Configuration for `LegacyStorageSnapshotManager`.
613#[derive(Debug, Clone)]
614pub struct SnapshotConfig {
615 /// Maximum number of non-deleted snapshots allowed at once.
616 pub max_snapshots: usize,
617 /// How many ticks a snapshot survives before being expired.
618 pub ttl_ticks: u64,
619 /// Whether `tick_cleanup` should automatically remove `Deleted` snapshots.
620 pub auto_cleanup: bool,
621}
622
623impl Default for SnapshotConfig {
624 fn default() -> Self {
625 Self {
626 max_snapshots: 10,
627 ttl_ticks: 1000,
628 auto_cleanup: true,
629 }
630 }
631}
632
633// ---------------------------------------------------------------------------
634// LegacySnapshotDiff
635// ---------------------------------------------------------------------------
636
637/// The difference between two legacy snapshots.
638#[derive(Clone, Debug, Default)]
639pub struct LegacySnapshotDiff {
640 /// CIDs present in snapshot B but absent from snapshot A.
641 pub added: Vec<String>,
642 /// CIDs present in snapshot A but absent from snapshot B.
643 pub removed: Vec<String>,
644 /// Number of CIDs common to both snapshots.
645 pub common: usize,
646}
647
648impl LegacySnapshotDiff {
649 /// Returns `true` when no blocks were added or removed.
650 pub fn is_empty(&self) -> bool {
651 self.added.is_empty() && self.removed.is_empty()
652 }
653}
654
655// ---------------------------------------------------------------------------
656// SnapshotManagerStats
657// ---------------------------------------------------------------------------
658
659/// Aggregate statistics reported by `LegacyStorageSnapshotManager::stats`.
660#[derive(Clone, Debug, Default)]
661pub struct SnapshotManagerStats {
662 /// Total number of snapshots currently held by the manager.
663 pub total_snapshots: usize,
664 /// Number of snapshots in `Ready` state.
665 pub ready_count: usize,
666 /// Number of snapshots in `Expired` state.
667 pub expired_count: usize,
668 /// Lifetime count of snapshots created.
669 pub total_created: u64,
670 /// Lifetime count of snapshots deleted.
671 pub total_deleted: u64,
672}
673
674// ---------------------------------------------------------------------------
675// LegacyStorageSnapshotManager
676// ---------------------------------------------------------------------------
677
678/// Legacy snapshot manager (kept for backwards compatibility).
679///
680/// Manages point-in-time storage snapshots with TTL, lifecycle states,
681/// auto-cleanup, and diff computation.
682pub struct LegacyStorageSnapshotManager {
683 config: SnapshotConfig,
684 snapshots: HashMap<u64, StorageSnapshot>,
685 next_id: u64,
686 current_tick: u64,
687 total_created: u64,
688 total_deleted: u64,
689}
690
691impl LegacyStorageSnapshotManager {
692 /// Create a new manager with the given configuration.
693 pub fn new(config: SnapshotConfig) -> Self {
694 Self {
695 config,
696 snapshots: HashMap::new(),
697 next_id: 1,
698 current_tick: 0,
699 total_created: 0,
700 total_deleted: 0,
701 }
702 }
703
704 /// Create a snapshot with the given label and block data.
705 ///
706 /// Returns the assigned snapshot id on success.
707 /// Returns an error if the maximum number of active (non-deleted)
708 /// snapshots has been reached.
709 pub fn create_snapshot(
710 &mut self,
711 label: &str,
712 block_cids: Vec<String>,
713 total_bytes: u64,
714 ) -> Result<u64, String> {
715 let active_count = self
716 .snapshots
717 .values()
718 .filter(|s| s.state != SnapshotState::Deleted)
719 .count();
720 if active_count >= self.config.max_snapshots {
721 return Err(format!(
722 "max snapshots reached ({})",
723 self.config.max_snapshots
724 ));
725 }
726
727 let id = self.next_id;
728 self.next_id += 1;
729 self.total_created += 1;
730
731 let block_count = block_cids.len();
732 let snapshot = StorageSnapshot {
733 id,
734 label: label.to_owned(),
735 state: SnapshotState::Ready,
736 created_tick: self.current_tick,
737 block_count,
738 total_bytes,
739 block_cids,
740 };
741 self.snapshots.insert(id, snapshot);
742 Ok(id)
743 }
744
745 /// Look up a snapshot by id.
746 pub fn get_snapshot(&self, id: u64) -> Option<&StorageSnapshot> {
747 self.snapshots.get(&id)
748 }
749
750 /// Mark a snapshot as `Deleted`.
751 pub fn delete_snapshot(&mut self, id: u64) -> Result<(), String> {
752 let snap = self
753 .snapshots
754 .get_mut(&id)
755 .ok_or_else(|| format!("snapshot {} not found", id))?;
756 if snap.state == SnapshotState::Deleted {
757 return Err(format!("snapshot {} already deleted", id));
758 }
759 snap.state = SnapshotState::Deleted;
760 self.total_deleted += 1;
761 Ok(())
762 }
763
764 /// Restore a snapshot, returning a clone of its block CIDs.
765 pub fn restore_snapshot(&self, id: u64) -> Result<Vec<String>, String> {
766 let snap = self
767 .snapshots
768 .get(&id)
769 .ok_or_else(|| format!("snapshot {} not found", id))?;
770 match snap.state {
771 SnapshotState::Ready => Ok(snap.block_cids.clone()),
772 SnapshotState::Expired => Err(format!("snapshot {} is expired", id)),
773 SnapshotState::Deleted => Err(format!("snapshot {} is deleted", id)),
774 SnapshotState::Creating => Err(format!("snapshot {} is still being created", id)),
775 }
776 }
777
778 /// List all snapshots as `(id, label, state)` tuples.
779 pub fn list_snapshots(&self) -> Vec<(u64, String, SnapshotState)> {
780 let mut list: Vec<(u64, String, SnapshotState)> = self
781 .snapshots
782 .values()
783 .map(|s| (s.id, s.label.clone(), s.state))
784 .collect();
785 list.sort_by_key(|(id, _, _)| *id);
786 list
787 }
788
789 /// Advance the internal tick, expire TTL-exceeded snapshots, and optionally
790 /// remove `Deleted` snapshots if `auto_cleanup` is enabled.
791 pub fn tick_cleanup(&mut self) {
792 self.current_tick += 1;
793
794 let ttl = self.config.ttl_ticks;
795 let current = self.current_tick;
796
797 for snap in self.snapshots.values_mut() {
798 if snap.state == SnapshotState::Ready && current.saturating_sub(snap.created_tick) > ttl
799 {
800 snap.state = SnapshotState::Expired;
801 }
802 }
803
804 if self.config.auto_cleanup {
805 self.snapshots
806 .retain(|_, s| s.state != SnapshotState::Deleted);
807 }
808 }
809
810 /// Count snapshots currently in `Ready` state.
811 pub fn ready_count(&self) -> usize {
812 self.snapshots
813 .values()
814 .filter(|s| s.state == SnapshotState::Ready)
815 .count()
816 }
817
818 /// Compute the diff between two snapshots identified by their ids.
819 pub fn diff_snapshots(&self, id_a: u64, id_b: u64) -> Result<LegacySnapshotDiff, String> {
820 use std::collections::HashSet;
821
822 let snap_a = self
823 .snapshots
824 .get(&id_a)
825 .ok_or_else(|| format!("snapshot {} not found", id_a))?;
826 let snap_b = self
827 .snapshots
828 .get(&id_b)
829 .ok_or_else(|| format!("snapshot {} not found", id_b))?;
830
831 let set_a: HashSet<&str> = snap_a.block_cids.iter().map(|s| s.as_str()).collect();
832 let set_b: HashSet<&str> = snap_b.block_cids.iter().map(|s| s.as_str()).collect();
833
834 let mut added: Vec<String> = set_b.difference(&set_a).map(|s| (*s).to_owned()).collect();
835 let mut removed: Vec<String> = set_a.difference(&set_b).map(|s| (*s).to_owned()).collect();
836 added.sort_unstable();
837 removed.sort_unstable();
838 let common = set_a.intersection(&set_b).count();
839
840 Ok(LegacySnapshotDiff {
841 added,
842 removed,
843 common,
844 })
845 }
846
847 /// Return aggregate statistics.
848 pub fn stats(&self) -> SnapshotManagerStats {
849 let mut ready_count = 0usize;
850 let mut expired_count = 0usize;
851 for snap in self.snapshots.values() {
852 match snap.state {
853 SnapshotState::Ready => ready_count += 1,
854 SnapshotState::Expired => expired_count += 1,
855 _ => {}
856 }
857 }
858 SnapshotManagerStats {
859 total_snapshots: self.snapshots.len(),
860 ready_count,
861 expired_count,
862 total_created: self.total_created,
863 total_deleted: self.total_deleted,
864 }
865 }
866}
867
868impl Default for LegacyStorageSnapshotManager {
869 fn default() -> Self {
870 Self::new(SnapshotConfig::default())
871 }
872}
873
874// ===========================================================================
875// Tests
876// ===========================================================================
877
878#[cfg(test)]
879mod tests {
880 use std::collections::HashMap;
881
882 use crate::snapshot_manager::SnapshotEntry;
883 use crate::snapshot_manager::{
884 compute_checksum, fnv1a_64, LegacySnapshotDiff, LegacyStorageSnapshotManager,
885 SnapshotConfig, SnapshotError, SnapshotId, SnapshotState, StorageSnapshotManager,
886 };
887
888 // -----------------------------------------------------------------------
889 // Helpers
890 // -----------------------------------------------------------------------
891
892 fn build_mgr(max: usize) -> StorageSnapshotManager {
893 StorageSnapshotManager::new(max)
894 }
895
896 // -----------------------------------------------------------------------
897 // 1. New empty manager has zero entries and zero snapshots
898 // -----------------------------------------------------------------------
899 #[test]
900 fn test_new_manager_is_empty() {
901 let mgr = build_mgr(10);
902 assert_eq!(mgr.live_entry_count(), 0);
903 assert_eq!(mgr.live_total_bytes(), 0);
904 assert_eq!(mgr.snapshot_count(), 0);
905 assert!(mgr.list_snapshots().is_empty());
906 }
907
908 // -----------------------------------------------------------------------
909 // 2. put inserts an entry and get retrieves it
910 // -----------------------------------------------------------------------
911 #[test]
912 fn test_put_and_get() {
913 let mut mgr = build_mgr(10);
914 mgr.put("alpha".into(), b"hello".to_vec(), 1);
915 assert_eq!(mgr.get("alpha"), Some(b"hello".as_slice()));
916 }
917
918 // -----------------------------------------------------------------------
919 // 3. get returns None for unknown key
920 // -----------------------------------------------------------------------
921 #[test]
922 fn test_get_missing_key() {
923 let mgr = build_mgr(10);
924 assert!(mgr.get("nope").is_none());
925 }
926
927 // -----------------------------------------------------------------------
928 // 4. delete removes an existing key
929 // -----------------------------------------------------------------------
930 #[test]
931 fn test_delete_existing_key() {
932 let mut mgr = build_mgr(10);
933 mgr.put("k".into(), b"v".to_vec(), 0);
934 let removed = mgr.delete("k");
935 assert!(removed);
936 assert!(mgr.get("k").is_none());
937 }
938
939 // -----------------------------------------------------------------------
940 // 5. delete returns false for non-existent key
941 // -----------------------------------------------------------------------
942 #[test]
943 fn test_delete_missing_key() {
944 let mut mgr = build_mgr(10);
945 assert!(!mgr.delete("ghost"));
946 }
947
948 // -----------------------------------------------------------------------
949 // 6. live_entry_count tracks additions and deletions
950 // -----------------------------------------------------------------------
951 #[test]
952 fn test_live_entry_count() {
953 let mut mgr = build_mgr(10);
954 assert_eq!(mgr.live_entry_count(), 0);
955 mgr.put("a".into(), b"1".to_vec(), 0);
956 mgr.put("b".into(), b"2".to_vec(), 0);
957 assert_eq!(mgr.live_entry_count(), 2);
958 mgr.delete("a");
959 assert_eq!(mgr.live_entry_count(), 1);
960 }
961
962 // -----------------------------------------------------------------------
963 // 7. live_total_bytes is sum of value lengths
964 // -----------------------------------------------------------------------
965 #[test]
966 fn test_live_total_bytes() {
967 let mut mgr = build_mgr(10);
968 mgr.put("x".into(), vec![0u8; 10], 0);
969 mgr.put("y".into(), vec![0u8; 20], 0);
970 assert_eq!(mgr.live_total_bytes(), 30);
971 }
972
973 // -----------------------------------------------------------------------
974 // 8. take_snapshot on empty state produces full snapshot with no delta
975 // -----------------------------------------------------------------------
976 #[test]
977 fn test_first_snapshot_is_full() {
978 let mut mgr = build_mgr(10);
979 let id = mgr.take_snapshot(None, 100);
980 let snap = mgr.get_snapshot(id).expect("must exist");
981 assert!(
982 snap.delta.is_none(),
983 "first snapshot must be full (no delta)"
984 );
985 }
986
987 // -----------------------------------------------------------------------
988 // 9. take_snapshot returns monotonically increasing ids
989 // -----------------------------------------------------------------------
990 #[test]
991 fn test_snapshot_ids_monotonic() {
992 let mut mgr = build_mgr(10);
993 let id1 = mgr.take_snapshot(None, 0);
994 let id2 = mgr.take_snapshot(None, 1);
995 let id3 = mgr.take_snapshot(None, 2);
996 assert!(id1 < id2);
997 assert!(id2 < id3);
998 }
999
1000 // -----------------------------------------------------------------------
1001 // 10. take_snapshot captures correct entry_count and total_bytes
1002 // -----------------------------------------------------------------------
1003 #[test]
1004 fn test_snapshot_entry_count_and_bytes() {
1005 let mut mgr = build_mgr(10);
1006 mgr.put("k1".into(), vec![1u8; 8], 0);
1007 mgr.put("k2".into(), vec![2u8; 16], 0);
1008 let id = mgr.take_snapshot(None, 0);
1009 let snap = mgr.get_snapshot(id).expect("must exist");
1010 assert_eq!(snap.entry_count, 2);
1011 assert_eq!(snap.total_bytes, 24);
1012 }
1013
1014 // -----------------------------------------------------------------------
1015 // 11. take_snapshot captures label
1016 // -----------------------------------------------------------------------
1017 #[test]
1018 fn test_snapshot_label() {
1019 let mut mgr = build_mgr(10);
1020 let id = mgr.take_snapshot(Some("my-label".to_owned()), 0);
1021 let snap = mgr.get_snapshot(id).expect("must exist");
1022 assert_eq!(snap.label.as_deref(), Some("my-label"));
1023 }
1024
1025 // -----------------------------------------------------------------------
1026 // 12. Second snapshot has a delta with added entries
1027 // -----------------------------------------------------------------------
1028 #[test]
1029 fn test_second_snapshot_delta_added() {
1030 let mut mgr = build_mgr(10);
1031 mgr.take_snapshot(None, 0); // full (empty)
1032 mgr.put("new".into(), b"data".to_vec(), 1);
1033 let id2 = mgr.take_snapshot(None, 2);
1034 let snap2 = mgr.get_snapshot(id2).expect("must exist");
1035 let delta = snap2
1036 .delta
1037 .as_ref()
1038 .expect("second snapshot must have delta");
1039 assert_eq!(delta.added.len(), 1);
1040 assert_eq!(delta.added[0].key, "new");
1041 assert!(delta.modified.is_empty());
1042 assert!(delta.deleted.is_empty());
1043 }
1044
1045 // -----------------------------------------------------------------------
1046 // 13. Delta captures modified entries
1047 // -----------------------------------------------------------------------
1048 #[test]
1049 fn test_snapshot_delta_modified() {
1050 let mut mgr = build_mgr(10);
1051 mgr.put("k".into(), b"v1".to_vec(), 0);
1052 mgr.take_snapshot(None, 0);
1053 mgr.put("k".into(), b"v2".to_vec(), 1);
1054 let id2 = mgr.take_snapshot(None, 1);
1055 let snap2 = mgr.get_snapshot(id2).expect("must exist");
1056 let delta = snap2.delta.as_ref().expect("must have delta");
1057 assert!(delta.added.is_empty());
1058 assert_eq!(delta.modified.len(), 1);
1059 assert_eq!(delta.modified[0].key, "k");
1060 assert_eq!(delta.modified[0].value, b"v2");
1061 assert!(delta.deleted.is_empty());
1062 }
1063
1064 // -----------------------------------------------------------------------
1065 // 14. Delta captures deleted entries
1066 // -----------------------------------------------------------------------
1067 #[test]
1068 fn test_snapshot_delta_deleted() {
1069 let mut mgr = build_mgr(10);
1070 mgr.put("gone".into(), b"bye".to_vec(), 0);
1071 mgr.take_snapshot(None, 0);
1072 mgr.delete("gone");
1073 let id2 = mgr.take_snapshot(None, 1);
1074 let snap2 = mgr.get_snapshot(id2).expect("must exist");
1075 let delta = snap2.delta.as_ref().expect("must have delta");
1076 assert!(delta.added.is_empty());
1077 assert!(delta.modified.is_empty());
1078 assert_eq!(delta.deleted, vec!["gone".to_owned()]);
1079 }
1080
1081 // -----------------------------------------------------------------------
1082 // 15. restore_snapshot rebuilds live state
1083 // -----------------------------------------------------------------------
1084 #[test]
1085 fn test_restore_snapshot_basic() {
1086 let mut mgr = build_mgr(10);
1087 mgr.put("a".into(), b"original".to_vec(), 0);
1088 let id = mgr.take_snapshot(None, 0);
1089
1090 // Mutate live state.
1091 mgr.put("a".into(), b"changed".to_vec(), 1);
1092 mgr.put("b".into(), b"new".to_vec(), 1);
1093 assert_eq!(mgr.get("a"), Some(b"changed".as_slice()));
1094
1095 // Restore to snapshot.
1096 mgr.restore_snapshot(id).expect("restore should succeed");
1097 assert_eq!(mgr.get("a"), Some(b"original".as_slice()));
1098 assert!(mgr.get("b").is_none(), "b was not in snapshot");
1099 }
1100
1101 // -----------------------------------------------------------------------
1102 // 16. restore_snapshot returns SnapshotNotFound for unknown id
1103 // -----------------------------------------------------------------------
1104 #[test]
1105 fn test_restore_snapshot_not_found() {
1106 let mut mgr = build_mgr(10);
1107 let result = mgr.restore_snapshot(SnapshotId(999));
1108 assert_eq!(result, Err(SnapshotError::SnapshotNotFound(999)));
1109 }
1110
1111 // -----------------------------------------------------------------------
1112 // 17. get_snapshot returns None for unknown id
1113 // -----------------------------------------------------------------------
1114 #[test]
1115 fn test_get_snapshot_unknown_id() {
1116 let mgr = build_mgr(10);
1117 assert!(mgr.get_snapshot(SnapshotId(42)).is_none());
1118 }
1119
1120 // -----------------------------------------------------------------------
1121 // 18. list_snapshots returns ordered oldest-to-newest
1122 // -----------------------------------------------------------------------
1123 #[test]
1124 fn test_list_snapshots_order() {
1125 let mut mgr = build_mgr(10);
1126 let id1 = mgr.take_snapshot(None, 0);
1127 let id2 = mgr.take_snapshot(None, 1);
1128 let id3 = mgr.take_snapshot(None, 2);
1129 let list = mgr.list_snapshots();
1130 assert_eq!(list.len(), 3);
1131 assert_eq!(list[0].id, id1);
1132 assert_eq!(list[1].id, id2);
1133 assert_eq!(list[2].id, id3);
1134 }
1135
1136 // -----------------------------------------------------------------------
1137 // 19. Oldest snapshot is evicted when max_snapshots is exceeded
1138 // -----------------------------------------------------------------------
1139 #[test]
1140 fn test_max_snapshots_eviction() {
1141 let mut mgr = build_mgr(3);
1142 let id1 = mgr.take_snapshot(None, 0);
1143 let id2 = mgr.take_snapshot(None, 1);
1144 let id3 = mgr.take_snapshot(None, 2);
1145 // id1 should still be present before overflow.
1146 assert!(mgr.get_snapshot(id1).is_some());
1147 // Add a 4th — id1 should be evicted.
1148 let id4 = mgr.take_snapshot(None, 3);
1149 assert!(mgr.get_snapshot(id1).is_none(), "id1 must be evicted");
1150 assert!(mgr.get_snapshot(id2).is_some());
1151 assert!(mgr.get_snapshot(id3).is_some());
1152 assert!(mgr.get_snapshot(id4).is_some());
1153 assert_eq!(mgr.snapshot_count(), 3);
1154 }
1155
1156 // -----------------------------------------------------------------------
1157 // 20. delete_snapshot removes the oldest snapshot
1158 // -----------------------------------------------------------------------
1159 #[test]
1160 fn test_delete_oldest_snapshot() {
1161 let mut mgr = build_mgr(10);
1162 let id1 = mgr.take_snapshot(None, 0);
1163 let _id2 = mgr.take_snapshot(None, 1);
1164 mgr.delete_snapshot(id1)
1165 .expect("delete oldest should succeed");
1166 assert!(mgr.get_snapshot(id1).is_none());
1167 assert_eq!(mgr.snapshot_count(), 1);
1168 }
1169
1170 // -----------------------------------------------------------------------
1171 // 21. delete_snapshot CannotDeleteNonOldest for non-oldest id
1172 // -----------------------------------------------------------------------
1173 #[test]
1174 fn test_delete_non_oldest_snapshot_errors() {
1175 let mut mgr = build_mgr(10);
1176 let _id1 = mgr.take_snapshot(None, 0);
1177 let id2 = mgr.take_snapshot(None, 1);
1178 let result = mgr.delete_snapshot(id2);
1179 assert_eq!(result, Err(SnapshotError::CannotDeleteNonOldest));
1180 }
1181
1182 // -----------------------------------------------------------------------
1183 // 22. delete_snapshot SnapshotNotFound for unknown id
1184 // -----------------------------------------------------------------------
1185 #[test]
1186 fn test_delete_snapshot_not_found() {
1187 let mut mgr = build_mgr(10);
1188 let result = mgr.delete_snapshot(SnapshotId(404));
1189 assert_eq!(result, Err(SnapshotError::SnapshotNotFound(404)));
1190 }
1191
1192 // -----------------------------------------------------------------------
1193 // 23. diff_snapshots correctly identifies changes between two snapshots
1194 // -----------------------------------------------------------------------
1195 #[test]
1196 fn test_diff_snapshots() {
1197 let mut mgr = build_mgr(10);
1198 mgr.put("common".into(), b"same".to_vec(), 0);
1199 mgr.put("will-change".into(), b"old".to_vec(), 0);
1200 mgr.put("will-delete".into(), b"bye".to_vec(), 0);
1201 let id_a = mgr.take_snapshot(None, 0);
1202
1203 mgr.put("will-change".into(), b"new".to_vec(), 1);
1204 mgr.delete("will-delete");
1205 mgr.put("added".into(), b"fresh".to_vec(), 1);
1206 let id_b = mgr.take_snapshot(None, 1);
1207
1208 let delta = mgr.diff_snapshots(id_a, id_b).expect("diff should succeed");
1209 assert_eq!(delta.added.len(), 1);
1210 assert_eq!(delta.added[0].key, "added");
1211 assert_eq!(delta.modified.len(), 1);
1212 assert_eq!(delta.modified[0].key, "will-change");
1213 assert_eq!(delta.deleted, vec!["will-delete".to_owned()]);
1214 }
1215
1216 // -----------------------------------------------------------------------
1217 // 24. diff_snapshots returns SnapshotNotFound for bad ids
1218 // -----------------------------------------------------------------------
1219 #[test]
1220 fn test_diff_snapshots_not_found() {
1221 let mut mgr = build_mgr(10);
1222 let id = mgr.take_snapshot(None, 0);
1223 assert_eq!(
1224 mgr.diff_snapshots(id, SnapshotId(999)),
1225 Err(SnapshotError::SnapshotNotFound(999))
1226 );
1227 assert_eq!(
1228 mgr.diff_snapshots(SnapshotId(999), id),
1229 Err(SnapshotError::SnapshotNotFound(999))
1230 );
1231 }
1232
1233 // -----------------------------------------------------------------------
1234 // 25. stats() reflects accurate counts
1235 // -----------------------------------------------------------------------
1236 #[test]
1237 fn test_stats_accuracy() {
1238 let mut mgr = build_mgr(10);
1239 mgr.put("a".into(), vec![0u8; 5], 0);
1240 mgr.put("b".into(), vec![0u8; 10], 0);
1241 let id1 = mgr.take_snapshot(None, 0);
1242 let _id2 = mgr.take_snapshot(None, 1);
1243
1244 let stats = mgr.stats();
1245 assert_eq!(stats.snapshot_count, 2);
1246 assert_eq!(stats.oldest_snapshot_id, Some(id1.0));
1247 assert_eq!(stats.live_entries, 2);
1248 assert_eq!(stats.live_bytes, 15);
1249 }
1250
1251 // -----------------------------------------------------------------------
1252 // 26. stats() on empty manager
1253 // -----------------------------------------------------------------------
1254 #[test]
1255 fn test_stats_empty() {
1256 let mgr = build_mgr(10);
1257 let stats = mgr.stats();
1258 assert_eq!(stats.snapshot_count, 0);
1259 assert!(stats.oldest_snapshot_id.is_none());
1260 assert!(stats.newest_snapshot_id.is_none());
1261 assert_eq!(stats.total_snapshot_bytes, 0);
1262 assert_eq!(stats.live_entries, 0);
1263 assert_eq!(stats.live_bytes, 0);
1264 }
1265
1266 // -----------------------------------------------------------------------
1267 // 27. checksum is deterministic
1268 // -----------------------------------------------------------------------
1269 #[test]
1270 fn test_checksum_deterministic() {
1271 let mut map1 = HashMap::new();
1272 map1.insert(
1273 "k1".to_owned(),
1274 SnapshotEntry {
1275 key: "k1".into(),
1276 value: b"val1".to_vec(),
1277 version: 1,
1278 },
1279 );
1280 map1.insert(
1281 "k2".to_owned(),
1282 SnapshotEntry {
1283 key: "k2".into(),
1284 value: b"val2".to_vec(),
1285 version: 2,
1286 },
1287 );
1288
1289 let mut map2 = HashMap::new();
1290 map2.insert(
1291 "k2".to_owned(),
1292 SnapshotEntry {
1293 key: "k2".into(),
1294 value: b"val2".to_vec(),
1295 version: 2,
1296 },
1297 );
1298 map2.insert(
1299 "k1".to_owned(),
1300 SnapshotEntry {
1301 key: "k1".into(),
1302 value: b"val1".to_vec(),
1303 version: 1,
1304 },
1305 );
1306
1307 assert_eq!(
1308 compute_checksum(&map1),
1309 compute_checksum(&map2),
1310 "checksum must be order-independent"
1311 );
1312 }
1313
1314 // -----------------------------------------------------------------------
1315 // 28. checksum changes when value changes
1316 // -----------------------------------------------------------------------
1317 #[test]
1318 fn test_checksum_changes_on_mutation() {
1319 let mut map1 = HashMap::new();
1320 map1.insert(
1321 "k".to_owned(),
1322 SnapshotEntry {
1323 key: "k".into(),
1324 value: b"original".to_vec(),
1325 version: 1,
1326 },
1327 );
1328 let c1 = compute_checksum(&map1);
1329
1330 let mut map2 = HashMap::new();
1331 map2.insert(
1332 "k".to_owned(),
1333 SnapshotEntry {
1334 key: "k".into(),
1335 value: b"changed!".to_vec(),
1336 version: 2,
1337 },
1338 );
1339 let c2 = compute_checksum(&map2);
1340
1341 assert_ne!(c1, c2);
1342 }
1343
1344 // -----------------------------------------------------------------------
1345 // 29. empty delta is_empty() returns true
1346 // -----------------------------------------------------------------------
1347 #[test]
1348 fn test_snapshot_delta_is_empty() {
1349 let mut mgr = build_mgr(10);
1350 mgr.put("x".into(), b"same".to_vec(), 0);
1351 let id1 = mgr.take_snapshot(None, 0);
1352 // No changes between id1 and id2.
1353 let id2 = mgr.take_snapshot(None, 1);
1354 let delta = mgr.diff_snapshots(id1, id2).expect("ok");
1355 assert!(delta.is_empty());
1356 }
1357
1358 // -----------------------------------------------------------------------
1359 // 30. restore to an earlier snapshot then re-snapshot preserves history
1360 // -----------------------------------------------------------------------
1361 #[test]
1362 fn test_restore_and_resnap() {
1363 let mut mgr = build_mgr(10);
1364 mgr.put("key".into(), b"v1".to_vec(), 0);
1365 let id1 = mgr.take_snapshot(Some("snap-1".into()), 0);
1366 mgr.put("key".into(), b"v2".to_vec(), 1);
1367 mgr.take_snapshot(Some("snap-2".into()), 1);
1368
1369 // Restore to snap-1.
1370 mgr.restore_snapshot(id1).expect("restore");
1371 assert_eq!(mgr.get("key"), Some(b"v1".as_slice()));
1372
1373 // Take a new snapshot after restore.
1374 let id3 = mgr.take_snapshot(Some("snap-3".into()), 2);
1375 let snap3 = mgr.get_snapshot(id3).expect("must exist");
1376 // snap3 is relative to snap-2 (the previous head in the deque).
1377 assert_eq!(snap3.entry_count, 1);
1378 }
1379
1380 // -----------------------------------------------------------------------
1381 // 31. Multiple puts to same key preserve latest version
1382 // -----------------------------------------------------------------------
1383 #[test]
1384 fn test_put_overwrites_value() {
1385 let mut mgr = build_mgr(10);
1386 mgr.put("k".into(), b"first".to_vec(), 0);
1387 mgr.put("k".into(), b"second".to_vec(), 0);
1388 mgr.put("k".into(), b"third".to_vec(), 0);
1389 assert_eq!(mgr.get("k"), Some(b"third".as_slice()));
1390 assert_eq!(mgr.live_entry_count(), 1);
1391 }
1392
1393 // -----------------------------------------------------------------------
1394 // 32. snapshot_count returns correct value
1395 // -----------------------------------------------------------------------
1396 #[test]
1397 fn test_snapshot_count() {
1398 let mut mgr = build_mgr(10);
1399 assert_eq!(mgr.snapshot_count(), 0);
1400 mgr.take_snapshot(None, 0);
1401 assert_eq!(mgr.snapshot_count(), 1);
1402 mgr.take_snapshot(None, 1);
1403 assert_eq!(mgr.snapshot_count(), 2);
1404 }
1405
1406 // -----------------------------------------------------------------------
1407 // 33. diff_snapshots on same id returns empty delta
1408 // -----------------------------------------------------------------------
1409 #[test]
1410 fn test_diff_same_snapshot() {
1411 let mut mgr = build_mgr(10);
1412 mgr.put("k".into(), b"v".to_vec(), 0);
1413 let id = mgr.take_snapshot(None, 0);
1414 let delta = mgr.diff_snapshots(id, id).expect("ok");
1415 assert!(delta.is_empty());
1416 }
1417
1418 // -----------------------------------------------------------------------
1419 // 34. stats newest_snapshot_id updated after multiple snapshots
1420 // -----------------------------------------------------------------------
1421 #[test]
1422 fn test_stats_newest_id() {
1423 let mut mgr = build_mgr(10);
1424 mgr.take_snapshot(None, 0);
1425 let id_last = mgr.take_snapshot(None, 1);
1426 let stats = mgr.stats();
1427 assert_eq!(stats.newest_snapshot_id, Some(id_last.0));
1428 }
1429
1430 // -----------------------------------------------------------------------
1431 // 35. snapshot checksum stored in SsmSnapshot
1432 // -----------------------------------------------------------------------
1433 #[test]
1434 fn test_snapshot_checksum_stored() {
1435 let mut mgr = build_mgr(10);
1436 mgr.put("ck".into(), b"data".to_vec(), 0);
1437 let id = mgr.take_snapshot(None, 0);
1438 let snap = mgr.get_snapshot(id).expect("must exist");
1439 assert_ne!(
1440 snap.checksum, 0,
1441 "checksum must be non-zero for non-empty state"
1442 );
1443 }
1444
1445 // -----------------------------------------------------------------------
1446 // 36. Snapshot created_at stores the provided timestamp
1447 // -----------------------------------------------------------------------
1448 #[test]
1449 fn test_snapshot_created_at() {
1450 let mut mgr = build_mgr(10);
1451 let id = mgr.take_snapshot(None, 1234567890);
1452 let snap = mgr.get_snapshot(id).expect("must exist");
1453 assert_eq!(snap.created_at, 1234567890);
1454 }
1455
1456 // -----------------------------------------------------------------------
1457 // Legacy API tests
1458 // -----------------------------------------------------------------------
1459
1460 fn default_legacy_config() -> SnapshotConfig {
1461 SnapshotConfig::default()
1462 }
1463
1464 fn small_legacy_config(max: usize, ttl: u64) -> SnapshotConfig {
1465 SnapshotConfig {
1466 max_snapshots: max,
1467 ttl_ticks: ttl,
1468 auto_cleanup: true,
1469 }
1470 }
1471
1472 // -----------------------------------------------------------------------
1473 // L1. Legacy create snapshot
1474 // -----------------------------------------------------------------------
1475 #[test]
1476 fn test_legacy_create_snapshot_fields() {
1477 let mut mgr = LegacyStorageSnapshotManager::new(default_legacy_config());
1478 let cids = vec!["cid_a".to_owned(), "cid_b".to_owned()];
1479 let id = mgr
1480 .create_snapshot("backup-1", cids.clone(), 300)
1481 .expect("should succeed");
1482
1483 let snap = mgr.get_snapshot(id).expect("snapshot must exist");
1484 assert_eq!(snap.id, id);
1485 assert_eq!(snap.label, "backup-1");
1486 assert_eq!(snap.state, SnapshotState::Ready);
1487 assert_eq!(snap.block_count, 2);
1488 assert_eq!(snap.total_bytes, 300);
1489 assert_eq!(snap.block_cids, cids);
1490 }
1491
1492 // -----------------------------------------------------------------------
1493 // L2. Legacy restore
1494 // -----------------------------------------------------------------------
1495 #[test]
1496 fn test_legacy_restore_snapshot() {
1497 let mut mgr = LegacyStorageSnapshotManager::new(default_legacy_config());
1498 let cids = vec!["a".into(), "b".into(), "c".into()];
1499 let id = mgr
1500 .create_snapshot("restore-me", cids.clone(), 100)
1501 .expect("ok");
1502 let restored = mgr.restore_snapshot(id).expect("restore should succeed");
1503 assert_eq!(restored, cids);
1504 }
1505
1506 // -----------------------------------------------------------------------
1507 // L3. Legacy diff — added
1508 // -----------------------------------------------------------------------
1509 #[test]
1510 fn test_legacy_diff_snapshots_added() {
1511 let mut mgr = LegacyStorageSnapshotManager::new(default_legacy_config());
1512 let id_a = mgr
1513 .create_snapshot("a", vec!["common".into()], 10)
1514 .expect("ok");
1515 let id_b = mgr
1516 .create_snapshot("b", vec!["common".into(), "new".into()], 20)
1517 .expect("ok");
1518 let diff: LegacySnapshotDiff = mgr.diff_snapshots(id_a, id_b).expect("ok");
1519 assert_eq!(diff.added, vec!["new".to_owned()]);
1520 assert!(diff.removed.is_empty());
1521 assert_eq!(diff.common, 1);
1522 }
1523
1524 // -----------------------------------------------------------------------
1525 // L4. Legacy TTL expiration
1526 // -----------------------------------------------------------------------
1527 #[test]
1528 fn test_legacy_ttl_expiration() {
1529 let mut mgr = LegacyStorageSnapshotManager::new(small_legacy_config(10, 2));
1530 let id = mgr.create_snapshot("ttl-test", vec![], 0).expect("ok");
1531 mgr.tick_cleanup();
1532 mgr.tick_cleanup();
1533 assert_eq!(
1534 mgr.get_snapshot(id).expect("exists").state,
1535 SnapshotState::Ready,
1536 );
1537 mgr.tick_cleanup();
1538 assert_eq!(
1539 mgr.get_snapshot(id).expect("exists").state,
1540 SnapshotState::Expired,
1541 );
1542 }
1543
1544 // -----------------------------------------------------------------------
1545 // L5. Legacy max snapshots limit
1546 // -----------------------------------------------------------------------
1547 #[test]
1548 fn test_legacy_max_snapshots() {
1549 let mut mgr = LegacyStorageSnapshotManager::new(small_legacy_config(2, 1000));
1550 mgr.create_snapshot("s1", vec![], 0).expect("ok");
1551 mgr.create_snapshot("s2", vec![], 0).expect("ok");
1552 let result = mgr.create_snapshot("s3", vec![], 0);
1553 assert!(result.is_err());
1554 }
1555
1556 // -----------------------------------------------------------------------
1557 // 37. fnv1a_64 is deterministic
1558 // -----------------------------------------------------------------------
1559 #[test]
1560 fn test_fnv1a_deterministic() {
1561 let h1 = fnv1a_64(b"hello");
1562 let h2 = fnv1a_64(b"hello");
1563 assert_eq!(h1, h2);
1564 assert_ne!(h1, fnv1a_64(b"world"));
1565 }
1566
1567 // -----------------------------------------------------------------------
1568 // 38. Snapshot with label=None stores None
1569 // -----------------------------------------------------------------------
1570 #[test]
1571 fn test_snapshot_no_label() {
1572 let mut mgr = build_mgr(10);
1573 let id = mgr.take_snapshot(None, 0);
1574 let snap = mgr.get_snapshot(id).expect("must exist");
1575 assert!(snap.label.is_none());
1576 }
1577
1578 // -----------------------------------------------------------------------
1579 // 39. delete_snapshot on single-element deque leaves it empty
1580 // -----------------------------------------------------------------------
1581 #[test]
1582 fn test_delete_only_snapshot() {
1583 let mut mgr = build_mgr(10);
1584 let id = mgr.take_snapshot(None, 0);
1585 mgr.delete_snapshot(id).expect("should succeed");
1586 assert_eq!(mgr.snapshot_count(), 0);
1587 // Now trying to delete again fails with NotFound.
1588 let result = mgr.delete_snapshot(id);
1589 assert_eq!(result, Err(SnapshotError::SnapshotNotFound(id.0)));
1590 }
1591
1592 // -----------------------------------------------------------------------
1593 // 40. SnapshotId Display formatting
1594 // -----------------------------------------------------------------------
1595 #[test]
1596 fn test_snapshot_id_display() {
1597 let sid = SnapshotId(42);
1598 assert_eq!(format!("{}", sid), "SnapshotId(42)");
1599 }
1600}