Skip to main content

bf_tree/
snapshot.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT license.
3
4use std::cell::UnsafeCell;
5use std::collections::{HashMap, VecDeque};
6use std::mem::{ManuallyDrop, MaybeUninit};
7use std::ops::{Deref, DerefMut};
8use std::panic;
9use std::path::Path;
10use std::path::PathBuf;
11use std::sync::Arc;
12
13#[cfg(not(all(feature = "shuttle", test)))]
14use rand::Rng;
15#[cfg(all(feature = "shuttle", test))]
16use shuttle::rand::Rng;
17
18#[cfg(unix)]
19use std::os::unix::fs::FileExt;
20#[cfg(windows)]
21use std::os::windows::fs::FileExt;
22
23#[cfg(any(feature = "metrics-rt-debug-all", feature = "metrics-rt-debug-timer"))]
24use thread_local::ThreadLocal;
25
26use crate::{
27    circular_buffer::CircularBuffer,
28    error::ConfigError,
29    fs::VfsImpl,
30    mini_page_op::LeafOperations,
31    nodes::{leaf_node::MiniPageNextLevel, LeafNode, INVALID_DISK_OFFSET},
32    nodes::{InnerNode, InnerNodeBuilder, PageID, DISK_PAGE_SIZE, INNER_NODE_SIZE},
33    storage::{make_vfs, LeafStorage, PageLocation, PageTable},
34    sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering},
35    sync::RwLock,
36    utils::{atomic_wait, get_rng, inner_lock::ReadGuard, BfsVisitor, NodeInfo},
37    wal::{LogEntry, LogEntryImpl, WriteAheadLog},
38    BfTree, Config, StorageBackend, WalConfig, WalReader,
39};
40// Used for macOS fallback sleep and tests
41#[allow(unused_imports)]
42use crate::sync::thread;
43
44const BF_TREE_MAGIC_BEGIN: &[u8; 16] = b"BF-TREE-V0-BEGIN";
45const BF_TREE_MAGIC_END: &[u8; 14] = b"BF-TREE-V0-END";
46const META_DATA_PAGE_OFFSET: usize = 0;
47
48const INVALID_SNAPSHOT_THREAD_ID: usize = usize::MAX; // Invalid thread slot id
49const NULL_PAGE_LOCATION_OFFSET: usize = usize::MAX; // Special page loc offset for a Null page
50const INVALID_SNAPSHOT_STATE: u64 = u64::MAX; // Invalid snapshot state
51pub const INVALID_SNAPSHOT_VERSION: u64 = u64::MAX >> 1; // Invalid snapshot version
52const DEFAULT_MAX_SNAPSHOT_THREAD_NUM: usize = 64; // Maximum numbers of concurrent threads in Bf-tree, if snapshot is enabled.
53const SNAPSHOT_STATE_PHASE_ID_SHIFT: usize = 61; // Number of bits to shift for phase id
54const SNAPSHOT_STATE_PHASE_NUM: u64 = 4; // There are 4 snapshot phases
55const SNAPSHOT_STATE_PHASE_ID_MASK: u64 = 0b111 << SNAPSHOT_STATE_PHASE_ID_SHIFT; // Most significant 3 bits for phase id (allowing up to 8 phases)
56const SNAPSHOT_STATE_VERSION_MASK: u64 = (1 << SNAPSHOT_STATE_PHASE_ID_SHIFT) - 1; // Least significant 61 bits for version number
57
58/// Snapshot phase identifier.
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub enum PhaseId {
61    Rest,
62    Prepare,
63    InProgress,
64    Sweep,
65}
66
67impl PhaseId {
68    fn from_raw(value: u64) -> Self {
69        match value {
70            0 => PhaseId::Rest,
71            1 => PhaseId::Prepare,
72            2 => PhaseId::InProgress,
73            3 => PhaseId::Sweep,
74            _ => panic!("Invalid phase id: {}", value),
75        }
76    }
77
78    fn as_raw(self) -> u64 {
79        match self {
80            PhaseId::Rest => 0,
81            PhaseId::Prepare => 1,
82            PhaseId::InProgress => 2,
83            PhaseId::Sweep => 3,
84        }
85    }
86}
87
88/// A simplified CPR snapshot of a Bf-Tree.
89/// For details, see the original CPR paper.
90/// When compared to the original CPR paper, the following simplifications were made:
91/// 1. No epoch framework, global state machine only.
92/// 2. No 2PL and partial execution of read/upsert/delete/ in one version allowed
93///    E.g., a bf-tree upsert operation requires a series of mini-transcational modifications m_0..m_n.
94///    We do not lock all m(s) beforehand. Also, we allow the operation to restart if at m_i CPR detects
95///    a version inconsistency (PREPARE/v thread seeing a (v+1) record). This is OK for bf-tree
96///    because all m(s) are independent even though a m_i could involve multiple page modifications.
97pub struct CPRSnapShotMgr {
98    // Snapshot state
99    // | 3 bits   |     61 bits    |
100    // | phase id | version number |
101    global_state: AtomicU64,
102    // False means the thread slot is vacant while True means occupied
103    thread_slots: [AtomicBool; DEFAULT_MAX_SNAPSHOT_THREAD_NUM],
104    // Thread-level snapshot state
105    thread_local_states: [AtomicU64; DEFAULT_MAX_SNAPSHOT_THREAD_NUM],
106    // Mappings of base/mini/inner nodes to their corresponding snapshot file offsets.
107    // Each user thread updates its own mappings asyncrhonously and get merged in the end.
108    thread_local_inner_mappings:
109        UnsafeCell<[Vec<(*const InnerNode, usize)>; DEFAULT_MAX_SNAPSHOT_THREAD_NUM]>,
110    thread_local_base_mappings: UnsafeCell<[Vec<(PageID, usize)>; DEFAULT_MAX_SNAPSHOT_THREAD_NUM]>,
111    thread_local_mini_mappings: UnsafeCell<[Vec<(PageID, usize)>; DEFAULT_MAX_SNAPSHOT_THREAD_NUM]>,
112    thread_local_mini_size_mappings:
113        UnsafeCell<[Vec<(PageID, usize)>; DEFAULT_MAX_SNAPSHOT_THREAD_NUM]>,
114    root_id: AtomicU64, // Page ID of the root node.
115    pause_snapshot: AtomicBool,
116    // The physical file of snapshot. Wrapped in RwLock so the underlying
117    // vfs can be swapped when taking a new snapshot.
118    // Arc<Box<dyn VfsImpl>> is another option.
119    vfs: RwLock<Arc<dyn VfsImpl>>,
120    // Ensuring only one snapshot is in progress at a time.
121    snapshot_in_progress: AtomicBool,
122    // Counter used for atomic wait/wake signaling. Incremented when threads release slots.
123    // The snapshot thread waits on this value; worker threads wake it after releasing slots.
124    phase_waiter: AtomicU32,
125}
126
127unsafe impl Sync for CPRSnapShotMgr {}
128
129unsafe impl Send for CPRSnapShotMgr {}
130
131/// Within the life time of this guard, bf-tree transactions are protected by CPR logic.
132pub struct CPRSnapshotGuard {
133    snapshot_mgr: Option<Arc<CPRSnapShotMgr>>,
134    thread_slot_id: usize,
135    snapshot_version: u64,
136    phase_id: PhaseId,
137}
138
139impl CPRSnapshotGuard {
140    pub fn new(snapshot_mgr: Option<Arc<CPRSnapShotMgr>>) -> Result<Self, ()> {
141        match snapshot_mgr {
142            None => Ok(Self {
143                snapshot_mgr: None,
144                thread_slot_id: INVALID_SNAPSHOT_THREAD_ID,
145                snapshot_version: INVALID_SNAPSHOT_VERSION,
146                phase_id: PhaseId::Rest,
147            }),
148            Some(ref mgr) => {
149                let (thread_slot_id, snapshot_version, phase_id) = mgr.reserve_thread_slot()?;
150
151                Ok(Self {
152                    snapshot_mgr: Some(mgr.clone()),
153                    thread_slot_id,
154                    snapshot_version,
155                    phase_id,
156                })
157            }
158        }
159    }
160
161    /// Returns the snapshot version for this thread's transaction.
162    pub fn snapshot_version(&self) -> u64 {
163        self.snapshot_version
164    }
165
166    /// Returns the phase id at the time the guard was acquired.
167    pub fn get_local_phase_id(&self) -> PhaseId {
168        self.phase_id
169    }
170
171    /// Returns true if the snapshot guard has a valid thread slot id.
172    pub fn is_protected(&self) -> bool {
173        self.thread_slot_id != INVALID_SNAPSHOT_THREAD_ID
174    }
175
176    pub fn snapshot_base_page(&self, id: PageID, ptr: &[u8], size: usize) {
177        self.snapshot_mgr
178            .as_ref()
179            .unwrap()
180            .snapshot_base_page(id, ptr, size, self.thread_slot_id);
181    }
182
183    pub fn snapshot_mini_page(&self, id: PageID, ptr: &[u8], size: usize) {
184        self.snapshot_mgr
185            .as_ref()
186            .unwrap()
187            .snapshot_mini_page(id, ptr, size, self.thread_slot_id);
188    }
189
190    pub fn snapshot_inner_node(&self, ptr: *const InnerNode) {
191        self.snapshot_mgr
192            .as_ref()
193            .unwrap()
194            .snapshot_inner_node(ptr, self.thread_slot_id);
195    }
196
197    pub fn snapshot_root_page(&self, root_id: PageID) {
198        self.snapshot_mgr
199            .as_ref()
200            .unwrap()
201            .snapshot_root_page(root_id);
202    }
203}
204
205impl Drop for CPRSnapshotGuard {
206    fn drop(&mut self) {
207        if let Some(ref mgr) = self.snapshot_mgr {
208            mgr.release_thread_slot(self.thread_slot_id);
209        }
210    }
211}
212
213impl CPRSnapShotMgr {
214    pub fn are_all_threads_in_next_version(&self) -> bool {
215        if !self.snapshot_in_progress.load(Ordering::Acquire) {
216            false
217        } else {
218            let global_phase_id = self.get_global_phase_id();
219            global_phase_id == PhaseId::Sweep || global_phase_id == PhaseId::Rest
220        }
221    }
222
223    /// Initialize a new snapshot instance.
224    pub fn new(version: u64) -> Self {
225        let vfs: Arc<dyn VfsImpl> = make_vfs(&StorageBackend::Memory, ":memory:");
226        Self {
227            global_state: AtomicU64::new(Self::new_snapshot_state(0, version)), // Initial state: (REST, version)
228            thread_slots: [const { AtomicBool::new(false) }; DEFAULT_MAX_SNAPSHOT_THREAD_NUM],
229            thread_local_states: [const { AtomicU64::new(INVALID_SNAPSHOT_STATE) };
230                DEFAULT_MAX_SNAPSHOT_THREAD_NUM],
231            thread_local_inner_mappings: UnsafeCell::new(
232                [const { Vec::new() }; DEFAULT_MAX_SNAPSHOT_THREAD_NUM],
233            ),
234            thread_local_base_mappings: UnsafeCell::new(
235                [const { Vec::new() }; DEFAULT_MAX_SNAPSHOT_THREAD_NUM],
236            ),
237            thread_local_mini_mappings: UnsafeCell::new(
238                [const { Vec::new() }; DEFAULT_MAX_SNAPSHOT_THREAD_NUM],
239            ),
240            thread_local_mini_size_mappings: UnsafeCell::new(
241                [const { Vec::new() }; DEFAULT_MAX_SNAPSHOT_THREAD_NUM],
242            ),
243            root_id: AtomicU64::new(0),
244            pause_snapshot: AtomicBool::new(false),
245            vfs: RwLock::new(vfs),
246            snapshot_in_progress: AtomicBool::new(false),
247            phase_waiter: AtomicU32::new(0),
248        }
249    }
250
251    /// Reset the snapshot
252    fn reset(&self) {
253        let local_inner_mappings = unsafe { &mut *self.thread_local_inner_mappings.get() };
254        let local_mini_mappings = unsafe { &mut *self.thread_local_mini_mappings.get() };
255        let local_base_mappings = unsafe { &mut *self.thread_local_base_mappings.get() };
256        let local_mini_size_mappings = unsafe { &mut *self.thread_local_mini_size_mappings.get() };
257        // De-duplicate mappings
258        for thread_slot_id in 0..DEFAULT_MAX_SNAPSHOT_THREAD_NUM {
259            local_inner_mappings[thread_slot_id] = Vec::new();
260            local_mini_mappings[thread_slot_id] = Vec::new();
261            local_base_mappings[thread_slot_id] = Vec::new();
262            local_mini_size_mappings[thread_slot_id] = Vec::new();
263        }
264
265        self.root_id.store(0, Ordering::Release);
266    }
267
268    pub fn new_snapshot_state(phase_id: u64, version: u64) -> u64 {
269        assert!(
270            phase_id < SNAPSHOT_STATE_PHASE_NUM,
271            "Phase id must be less than {}",
272            SNAPSHOT_STATE_PHASE_NUM
273        );
274        assert!(
275            version < (1 << SNAPSHOT_STATE_PHASE_ID_SHIFT),
276            "Version must be less than 2^61"
277        );
278        (phase_id << SNAPSHOT_STATE_PHASE_ID_SHIFT) | version
279    }
280
281    fn get_global_version(&self) -> u64 {
282        self.global_state.load(Ordering::Acquire) & SNAPSHOT_STATE_VERSION_MASK
283    }
284
285    fn get_global_phase_id(&self) -> PhaseId {
286        PhaseId::from_raw(
287            (self.global_state.load(Ordering::Acquire) & SNAPSHOT_STATE_PHASE_ID_MASK)
288                >> SNAPSHOT_STATE_PHASE_ID_SHIFT,
289        )
290    }
291
292    /// Retrieve the local state of a thread specified by its slot id.
293    fn get_local_state(&self, thread_slot_id: &usize) -> u64 {
294        self.thread_local_states[*thread_slot_id].load(Ordering::Acquire)
295    }
296
297    fn set_local_state(&self, thread_slot_id: &usize, state: u64) {
298        // Don't dirty the existing state if it's the same.
299        let current_state = self.get_local_state(thread_slot_id);
300        if current_state == state {
301            return;
302        }
303
304        self.thread_local_states[*thread_slot_id].store(state, Ordering::Release);
305    }
306
307    /// Move the global snapshot stable to the next one.
308    fn advance_global_state(&self) -> u64 {
309        let phase_id = self.get_global_phase_id();
310        let version = self.get_global_version();
311
312        let new_state = match phase_id {
313            PhaseId::Rest => {
314                // (REST, v) -> (PREPARE, v)
315                Self::new_snapshot_state(PhaseId::Prepare.as_raw(), version)
316            }
317            PhaseId::Prepare => {
318                // (PREPARE, v) -> (IN_PROGRESS, v + 1)
319                Self::new_snapshot_state(PhaseId::InProgress.as_raw(), version + 1)
320            }
321            PhaseId::InProgress => {
322                // (IN_PROGRESS, v + 1) -> (SWEEPING, v + 1)
323                Self::new_snapshot_state(PhaseId::Sweep.as_raw(), version)
324            }
325            PhaseId::Sweep => {
326                // (SWEEPING, v + 1) -> (REST, v + 1)
327                Self::new_snapshot_state(PhaseId::Rest.as_raw(), version)
328            }
329        };
330
331        // Advance the global state
332        self.global_state.store(new_state, Ordering::Release);
333
334        new_state
335    }
336
337    /// If all thread local states are either invalid or equal to the target state, return true.
338    /// This can only be invoked after the global state has advanced to the target_state.
339    fn check_if_phase_completed(&self, target_state: u64) -> bool {
340        // Checking all thread local states is sufficient because of the guarantee in `reserve_thread_slot`
341        for thread_slot_id in 0..DEFAULT_MAX_SNAPSHOT_THREAD_NUM {
342            let local_state = self.thread_local_states[thread_slot_id].load(Ordering::Acquire);
343            if local_state != INVALID_SNAPSHOT_STATE && local_state != target_state {
344                return false;
345            }
346        }
347        true
348    }
349
350    /// Wait for all threads in the old phase to complete their transition.
351    /// Uses atomic wait instead of polling with fixed sleep.
352    fn wait_for_phase_completion(&self, target_state: u64) {
353        loop {
354            // Check if all threads have transitioned
355            if self.check_if_phase_completed(target_state) {
356                return;
357            }
358
359            // Capture current waiter value before waiting
360            let waiter_val = self.phase_waiter.load(Ordering::Acquire);
361
362            // Double-check after loading waiter (avoid missed wake)
363            if self.check_if_phase_completed(target_state) {
364                return;
365            }
366
367            // Wait for notification (futex on Linux, WaitOnAddress on Windows)
368            atomic_wait::wait(&self.phase_waiter, waiter_val);
369
370            // Fallback short sleep for platforms where atomic_wait is a no-op (e.g., macOS)
371            #[cfg(target_os = "macos")]
372            thread::sleep(std::time::Duration::from_millis(1));
373        }
374    }
375
376    /// Wait for all threads to release their slots (all local states become INVALID).
377    /// Used during sweep pause when we need to drain all active threads.
378    fn wait_for_all_slots_released(&self) {
379        loop {
380            // Check if all slots are released
381            if self.check_if_phase_completed(INVALID_SNAPSHOT_STATE) {
382                return;
383            }
384
385            // Capture current waiter value before waiting
386            let waiter_val = self.phase_waiter.load(Ordering::Acquire);
387
388            // Double-check after loading waiter
389            if self.check_if_phase_completed(INVALID_SNAPSHOT_STATE) {
390                return;
391            }
392
393            // Wait for any thread to release its slot
394            atomic_wait::wait(&self.phase_waiter, waiter_val);
395
396            // Fallback short sleep for platforms where atomic_wait is a no-op
397            #[cfg(target_os = "macos")]
398            thread::sleep(std::time::Duration::from_millis(1));
399        }
400    }
401
402    /// Obtain an unique thread slot id for the caller thread.
403    /// Guarantee that any local state assigned after the global state advances to the next one,
404    /// will either be reversed without further action or in the new state.
405    pub fn reserve_thread_slot(&self) -> Result<(usize, u64, PhaseId), ()> {
406        if self.pause_snapshot.load(Ordering::Acquire) {
407            return Err(());
408        }
409
410        let start = get_rng().random_range(0..DEFAULT_MAX_SNAPSHOT_THREAD_NUM);
411        let end = 2 * DEFAULT_MAX_SNAPSHOT_THREAD_NUM;
412
413        for i in start..end {
414            let tid = i % DEFAULT_MAX_SNAPSHOT_THREAD_NUM;
415            if self.thread_slots[tid]
416                .compare_exchange(false, true, Ordering::AcqRel, Ordering::Relaxed)
417                .is_ok()
418            {
419                // Set the caller thread's snapshot local state to the global state
420                let global_state = self.global_state.load(Ordering::Acquire);
421                self.set_local_state(&tid, global_state);
422
423                // If the global state has changed or a pause is requested after setting the local state, reset
424                // This is to guarantee that as soon as global state rolls to the next one,
425                // all new local states will either be reversed without further action or in the new state.
426                // Otherwise something bad could happen as described below:
427                // T1: state = global phase 'x'
428                // Mgr: global phase <- 'x + 1'
429                // Mrgr: all threads in phase 'x + 1' or invalid -> Execute 'x + 1' action
430                // T1: local state = state <- Inconsistency with global state
431                // Similar case for the pause_snapshot flag.
432                let current_global = self.global_state.load(Ordering::Acquire);
433                if self.get_local_state(&tid) != current_global
434                    || self.pause_snapshot.load(Ordering::Acquire)
435                {
436                    self.set_local_state(&tid, INVALID_SNAPSHOT_STATE);
437                    assert!(self.thread_slots[tid]
438                        .compare_exchange(true, false, Ordering::AcqRel, Ordering::Relaxed)
439                        .is_ok());
440
441                    // Wake the snapshot thread since we released a slot
442                    self.phase_waiter.fetch_add(1, Ordering::Release);
443                    atomic_wait::wake_all(&self.phase_waiter);
444
445                    return Err(());
446                } else {
447                    let version = global_state & SNAPSHOT_STATE_VERSION_MASK;
448                    let phase_id = PhaseId::from_raw(
449                        (global_state & SNAPSHOT_STATE_PHASE_ID_MASK)
450                            >> SNAPSHOT_STATE_PHASE_ID_SHIFT,
451                    );
452                    return Ok((tid, version, phase_id));
453                }
454            }
455        }
456        Err(())
457    }
458
459    /// Free up the thread slot specified by the given thread slot id.
460    /// Always notifies waiting snapshot thread via phase_waiter.
461    pub fn release_thread_slot(&self, thread_slot_id: usize) {
462        // Clear the local state and slot
463        self.set_local_state(&thread_slot_id, INVALID_SNAPSHOT_STATE);
464        self.thread_slots[thread_slot_id].store(false, Ordering::Release);
465
466        // Always wake the snapshot thread - it will verify completion via slot scan.
467        // This is simpler and avoids race conditions with counter-based approaches.
468        self.phase_waiter.fetch_add(1, Ordering::Release);
469        atomic_wait::wake_all(&self.phase_waiter);
470    }
471
472    pub fn get_snapshot_guard(
473        snapshot_mgr: Option<Arc<CPRSnapShotMgr>>,
474    ) -> Result<CPRSnapshotGuard, ()> {
475        CPRSnapshotGuard::new(snapshot_mgr)
476    }
477
478    /// Snapshot a page to the current snapshot file and return its offset in the file.
479    /// The invoker needs to guarantee that the page to be copied is xlocked
480    /// throughout the lifetime of this function.
481    /// Also the invoker needs to guarantee proper alignment of ptr which could be required
482    /// by the underlying vfs. (E.g., io_uring_vfs requires 512B alignment)
483    pub fn snapshot_page(&self, ptr: &[u8], size: usize) -> usize {
484        // Allocate space in the snapshot file
485        let vfs = self.vfs.read().unwrap().clone();
486        let offset = vfs.alloc_offset(size);
487
488        // Copy the page (ptr) to the new space
489        vfs.write(offset, ptr);
490
491        // Return the offset
492        offset
493    }
494
495    pub fn snapshot_inner_node(&self, ptr: *const InnerNode, thread_slot_id: usize) {
496        let offset = unsafe { self.snapshot_page((&*ptr).as_slice(), INNER_NODE_SIZE) };
497        let inner_mappings = unsafe { &mut *self.thread_local_inner_mappings.get() };
498
499        inner_mappings[thread_slot_id].push((ptr, offset));
500    }
501
502    pub fn snapshot_mini_page(&self, id: PageID, ptr: &[u8], size: usize, thread_slot_id: usize) {
503        let offset = if size != 0 {
504            self.snapshot_page(ptr, size)
505        } else {
506            // cache-only mode, NULL page
507            NULL_PAGE_LOCATION_OFFSET
508        };
509
510        let mini_mappings = unsafe { &mut *self.thread_local_mini_mappings.get() };
511        mini_mappings[thread_slot_id].push((id, offset));
512
513        let mini_size_mappings = unsafe { &mut *self.thread_local_mini_size_mappings.get() };
514        mini_size_mappings[thread_slot_id].push((id, size));
515
516        assert!(mini_mappings[thread_slot_id].len() == mini_size_mappings[thread_slot_id].len());
517    }
518
519    pub fn snapshot_base_page(&self, id: PageID, ptr: &[u8], size: usize, thread_slot_id: usize) {
520        let offset = self.snapshot_page(ptr, size);
521
522        let base_mappings = unsafe { &mut *self.thread_local_base_mappings.get() };
523        base_mappings[thread_slot_id].push((id, offset));
524    }
525
526    pub fn snapshot_root_page(&self, root_id: PageID) {
527        let cur_root_id = self.root_id.load(Ordering::Acquire);
528        if cur_root_id != 0 {
529            assert_eq!(cur_root_id, root_id.raw());
530        }
531
532        self.root_id.store(root_id.raw(), Ordering::Release);
533    }
534
535    /// Sweep through all data pages and take snapshots of those
536    /// whose version is less than the passed-in snapshot version.
537    fn sweep(
538        &self,
539        tree: &BfTree,
540        version: u64,
541        inner_mapping: &mut Vec<(*const InnerNode, usize)>,
542        mini_mapping: &mut Vec<(PageID, usize)>,
543        mini_size_mapping: &mut Vec<(PageID, usize)>,
544        base_mapping: &mut Vec<(PageID, usize)>,
545    ) -> usize {
546        // There is no page table for inner nodes including the root, and each inner node's information is only
547        // saved in the tree structure itself. As a result, we need to traverse the tree to find all inner nodes.
548        // However, without blocking inner node splitting, the tree structure could change concurrently while we
549        // are BFS/DFSing the tree, making it difficult to enumerate all inner nodes to build inner node mappings.
550        // As such we freeze the tree structure temporarily using a 3-phase approach.
551        // Phase 1: Block all snapshot id reservation, drain the thread table.
552        // Phase 2: Traverse the tree and take snapshots of inner nodes whose version is < 'version'.
553        // Phase 3: Unblock snapshot id reservation.
554        // Given that there are usually a very limited number of inner nodes, user workload should be affected minimally.
555        // TODO: A complete block-free approach to scan all inner nodes.
556        self.pause_snapshot.store(true, Ordering::Release);
557        loop {
558            if self.check_if_phase_completed(INVALID_SNAPSHOT_STATE) {
559                // Upon reaching here, no user threads can obtain a snapshot id nor making changes to the tree structure.
560                // We sweep the root node first
561                loop {
562                    let root_id = tree.get_root_page();
563                    let rid = root_id.0;
564                    if root_id.1 {
565                        // Leaf
566                        let mut leaf = tree.mapping_table().get(&rid);
567                        let page_loc = leaf.get_page_location();
568
569                        match page_loc {
570                            PageLocation::Base(offset) => {
571                                let base_ref = leaf.load_base_page(*offset);
572                                if base_ref.get_clean_snapshot_version() < version {
573                                    let base_ptr = unsafe {
574                                        std::slice::from_raw_parts(
575                                            base_ref as *const LeafNode as *const u8,
576                                            base_ref.meta.node_size as usize,
577                                        )
578                                    };
579                                    let offset = self
580                                        .snapshot_page(base_ptr, base_ref.meta.node_size as usize);
581                                    base_mapping.push((rid, offset));
582                                    self.snapshot_root_page(rid);
583                                }
584                            }
585                            PageLocation::Mini(ptr) => {
586                                // Root page is a mini page only in cache-only mode.
587                                assert!(tree.cache_only);
588
589                                let mini_ref = leaf.load_cache_page(*ptr);
590                                if mini_ref.get_clean_snapshot_version() < version {
591                                    let mini_ptr = unsafe {
592                                        std::slice::from_raw_parts(
593                                            mini_ref as *const LeafNode as *const u8,
594                                            mini_ref.meta.node_size as usize,
595                                        )
596                                    };
597                                    let offset = self
598                                        .snapshot_page(mini_ptr, mini_ref.meta.node_size as usize);
599                                    mini_mapping.push((rid, offset));
600                                    mini_size_mapping.push((rid, mini_ref.meta.node_size as usize));
601                                    self.snapshot_root_page(rid);
602                                }
603                            }
604                            _ => {
605                                panic!("Unexpected page location for root page: {:?}", page_loc);
606                            }
607                        }
608
609                        break;
610                    } else {
611                        // Inner
612                        let ptr = rid.as_inner_node();
613                        // No need for WriteGuard as the tree structured is frozen and there are no active writers.
614                        let inner = match ReadGuard::try_read(ptr) {
615                            Ok(inner) => inner,
616                            Err(_) => continue,
617                        };
618
619                        if inner.as_ref().get_clean_snapshot_version() < version {
620                            let offset =
621                                unsafe { self.snapshot_page((&*ptr).as_slice(), INNER_NODE_SIZE) };
622                            inner_mapping.push((ptr, offset));
623                            self.snapshot_root_page(rid);
624                        }
625                        break;
626                    }
627                }
628
629                // Inner nodes
630                let visitor = BfsVisitor::new_inner_only(tree);
631                for node in visitor {
632                    loop {
633                        match node {
634                            NodeInfo::Inner { ptr, .. } => {
635                                // No need for WriteGuard as the tree structured is frozen and there are no active writers.
636                                let inner = match ReadGuard::try_read(ptr) {
637                                    Ok(inner) => inner,
638                                    Err(_) => continue,
639                                };
640
641                                if inner.as_ref().get_clean_snapshot_version() < version {
642                                    let offset = unsafe {
643                                        self.snapshot_page((&*ptr).as_slice(), INNER_NODE_SIZE)
644                                    };
645                                    inner_mapping.push((ptr, offset));
646                                }
647
648                                break;
649                            }
650                            NodeInfo::Leaf { level, .. } => {
651                                // This should have been captured by the case when root node is a leaf
652                                assert_eq!(level, 0);
653                                break;
654                            }
655                        }
656                    }
657                }
658
659                break;
660            }
661            // Wait for all threads to release their slots (no polling)
662            self.wait_for_all_slots_released();
663
664            #[cfg(all(feature = "shuttle", test))]
665            shuttle::thread::yield_now();
666        }
667
668        // Resume workload
669        self.pause_snapshot.store(false, Ordering::Release);
670
671        // In SWEEPING phase, there will be no new disk pages with v < 'version' being created. As such,
672        // a sequential sweep of the page table is sufficient to capture all data pages with v < 'version'.
673        let page_table_iter = tree.storage.page_table.iter();
674        let mut enumerate_leaf_count = 0;
675
676        for (_, pid) in page_table_iter {
677            assert!(pid.is_id());
678
679            // A reader lock is enough
680            let mut leaf = tree.mapping_table().get(&pid);
681            let page_loc = leaf.get_page_location().clone();
682            enumerate_leaf_count += 1;
683
684            match page_loc {
685                PageLocation::Base(offset) => {
686                    let base_ref = leaf.load_base_page(offset);
687                    if base_ref.get_clean_snapshot_version() < version {
688                        let base_ptr = unsafe {
689                            std::slice::from_raw_parts(
690                                base_ref as *const LeafNode as *const u8,
691                                base_ref.meta.node_size as usize,
692                            )
693                        };
694                        let new_offset =
695                            self.snapshot_page(base_ptr, base_ref.meta.node_size as usize);
696                        base_mapping.push((pid, new_offset));
697                    }
698                }
699                PageLocation::Full(ptr) => {
700                    // We snapshot Full page as a disk page to reduce some complexity as they are equivalent.
701                    let full_ref = leaf.load_cache_page(ptr);
702                    if full_ref.get_clean_snapshot_version() < version {
703                        // Temporarily change the next level to null for snapshotting.
704                        // and reverse afterwards.
705                        let next_level = full_ref.next_level;
706                        let full_page = unsafe { &mut *ptr };
707                        full_page.next_level = MiniPageNextLevel::new_null();
708                        let full_ptr = unsafe {
709                            std::slice::from_raw_parts(
710                                full_ref as *const LeafNode as *const u8,
711                                full_ref.meta.node_size as usize,
712                            )
713                        };
714                        let offset = self.snapshot_page(full_ptr, full_ref.meta.node_size as usize);
715                        full_page.next_level = next_level;
716                        base_mapping.push((pid, offset));
717                    }
718                }
719                PageLocation::Mini(ptr) => {
720                    let mini_ref = leaf.load_cache_page(ptr);
721                    if mini_ref.get_clean_snapshot_version() < version {
722                        let mini_ptr = unsafe {
723                            std::slice::from_raw_parts(
724                                mini_ref as *const LeafNode as *const u8,
725                                mini_ref.meta.node_size as usize,
726                            )
727                        };
728                        let offset = self.snapshot_page(mini_ptr, mini_ref.meta.node_size as usize);
729                        mini_mapping.push((pid, offset));
730                        mini_size_mapping.push((pid, mini_ref.meta.node_size as usize));
731
732                        if !tree.cache_only {
733                            // In disk-mode, the base page of mini-page is part of the snapshot as well.
734                            let base_ref = leaf.load_base_page(mini_ref.next_level.as_offset());
735                            assert!(base_ref.get_clean_snapshot_version() < version); // disk page's version should never be greater than its mini-page's.
736
737                            let base_ptr = unsafe {
738                                std::slice::from_raw_parts(
739                                    base_ref as *const LeafNode as *const u8,
740                                    base_ref.meta.node_size as usize,
741                                )
742                            };
743                            let offset =
744                                self.snapshot_page(base_ptr, base_ref.meta.node_size as usize);
745                            base_mapping.push((pid, offset));
746                        }
747                    }
748                }
749                PageLocation::Null => {
750                    assert!(tree.cache_only);
751                    // In cache-only mode, an entry in page table could be Null when the corresponding mini-page is evicted.
752                    // The Null page is also snapshotted with a special marker.
753                    // Note that, the underlying assumption here is that Null page is always of an older version which may not be true.
754                    // To reconcille with the CPR semantics, we say that any data page in cache-only mode could be either Null or a valid page.
755                    // TODO: version the Null pages.
756                    mini_mapping.push((pid, NULL_PAGE_LOCATION_OFFSET)); // Special marker
757                    mini_size_mapping.push((pid, 0));
758                }
759            }
760        }
761
762        enumerate_leaf_count
763    }
764
765    /// Finalize the snapshot file and reset the snapshotmgr's internal data.
766    #[allow(clippy::too_many_arguments)]
767    fn finalize(
768        &self,
769        snapshot_version: u64,
770        inner_mapping: &mut [(*const InnerNode, usize)],
771        mini_mapping: &mut [(PageID, usize)],
772        mini_size_mapping: &mut [(PageID, usize)],
773        base_mapping: &mut [(PageID, usize)],
774        leaf_count_upper: usize,
775        config: Arc<Config>,
776    ) {
777        // There could be duplicate leaf/inner node mappings and we use a hash map to de-duplicate them.
778        let mut inner_mapping_unique: HashMap<*const InnerNode, usize> = HashMap::new();
779        let mut mini_mapping_unique: HashMap<PageID, usize> = HashMap::new();
780        let mut mini_size_mapping_unique: HashMap<PageID, usize> = HashMap::new();
781        let mut base_mapping_unique: HashMap<PageID, usize> = HashMap::new();
782
783        let local_inner_mappings = unsafe { &mut *self.thread_local_inner_mappings.get() };
784        let local_mini_mappings = unsafe { &mut *self.thread_local_mini_mappings.get() };
785        let local_mini_size_mappings = unsafe { &mut *self.thread_local_mini_size_mappings.get() };
786        let local_base_mappings = unsafe { &mut *self.thread_local_base_mappings.get() };
787
788        for thread_slot_id in 0..DEFAULT_MAX_SNAPSHOT_THREAD_NUM {
789            let entry_num = local_mini_mappings[thread_slot_id].len();
790            assert!(
791                local_mini_mappings[thread_slot_id].len()
792                    == local_mini_size_mappings[thread_slot_id].len()
793            );
794            for i in 0..entry_num {
795                assert!(
796                    local_mini_mappings[thread_slot_id][i].0
797                        == local_mini_size_mappings[thread_slot_id][i].0
798                );
799
800                if local_mini_mappings[thread_slot_id][i].1 == NULL_PAGE_LOCATION_OFFSET {
801                    assert_eq!(local_mini_size_mappings[thread_slot_id][i].1, 0);
802                } else {
803                    assert!(local_mini_size_mappings[thread_slot_id][i].1 > 0);
804                }
805
806                if let std::collections::hash_map::Entry::Vacant(e) =
807                    mini_mapping_unique.entry(local_mini_mappings[thread_slot_id][i].0)
808                {
809                    e.insert(local_mini_mappings[thread_slot_id][i].1);
810                    mini_size_mapping_unique.insert(
811                        local_mini_size_mappings[thread_slot_id][i].0,
812                        local_mini_size_mappings[thread_slot_id][i].1,
813                    );
814                    assert!(mini_mapping_unique.len() == mini_size_mapping_unique.len());
815                }
816            }
817            assert_eq!(local_mini_mappings[thread_slot_id].len(), entry_num);
818            assert_eq!(local_mini_size_mappings[thread_slot_id].len(), entry_num);
819        }
820
821        // De-duplicate mappings among snapshot threads
822        for thread_slot_id in 0..DEFAULT_MAX_SNAPSHOT_THREAD_NUM {
823            for m in local_inner_mappings[thread_slot_id].iter() {
824                inner_mapping_unique.entry(m.0).or_insert(m.1);
825            }
826
827            for m in local_base_mappings[thread_slot_id].iter() {
828                base_mapping_unique.entry(m.0).or_insert(m.1);
829            }
830        }
831
832        // Sanity checks
833        assert!(mini_mapping_unique.len() == mini_size_mapping_unique.len());
834        for (k, v) in mini_mapping_unique.iter() {
835            assert!(mini_size_mapping_unique.contains_key(k));
836            if *v == NULL_PAGE_LOCATION_OFFSET {
837                assert_eq!(mini_size_mapping_unique.get(k).copied().unwrap(), 0);
838            } else {
839                assert!(mini_size_mapping_unique.get(k).copied().unwrap() > 0);
840            }
841        }
842
843        // De-duplicate with the sweep mappings
844        for (k, v) in inner_mapping.iter() {
845            if !inner_mapping_unique.contains_key(k) {
846                inner_mapping_unique.insert(*k, *v);
847            }
848        }
849        for (k, v) in mini_mapping.iter() {
850            if !mini_mapping_unique.contains_key(k) {
851                mini_mapping_unique.insert(*k, *v);
852            }
853        }
854        for (k, v) in mini_size_mapping.iter() {
855            if !mini_size_mapping_unique.contains_key(k) {
856                mini_size_mapping_unique.insert(*k, *v);
857            } else {
858                if !config.cache_only {
859                    assert!(*v == mini_size_mapping_unique.get(k).copied().unwrap());
860                }
861            }
862        }
863        for (k, v) in base_mapping.iter() {
864            if !base_mapping_unique.contains_key(k) {
865                base_mapping_unique.insert(*k, *v);
866            }
867        }
868
869        // Sanity checks
870        assert!(mini_mapping_unique.len() == mini_size_mapping_unique.len());
871        for (k, v) in mini_mapping_unique.iter() {
872            assert!(mini_size_mapping_unique.contains_key(k));
873            if *v == NULL_PAGE_LOCATION_OFFSET {
874                assert_eq!(mini_size_mapping_unique.get(k).copied().unwrap(), 0);
875            } else {
876                assert!(mini_size_mapping_unique.get(k).copied().unwrap() > 0);
877            }
878        }
879
880        if config.cache_only {
881            assert!(base_mapping_unique.is_empty());
882        } else {
883            assert!(mini_mapping_unique.len() <= base_mapping_unique.len());
884        }
885
886        // Finalize the inner node mappings of the snapshot
887        let mut final_inner_mapping: Vec<(*const InnerNode, usize)> = Vec::new();
888        for (k, v) in inner_mapping_unique.into_iter() {
889            final_inner_mapping.push((k, v));
890        }
891
892        // Finalize the base leaf node mappings of the snapshot, disk-mode only
893        // Sort the base leaf node mappings by PageID in ascending order
894        // which is required for page table initialization.
895        let mut sorted_base_mapping_uninit: Vec<MaybeUninit<(PageID, usize)>> =
896            Vec::with_capacity(base_mapping_unique.len());
897        unsafe {
898            sorted_base_mapping_uninit.set_len(base_mapping_unique.len());
899        }
900        let mut sorted_base_mapping_init = vec![false; base_mapping_unique.len()];
901
902        for (k, v) in base_mapping_unique.iter() {
903            assert!(k.is_id());
904            let offset = k.as_id();
905            assert!((offset as usize) < sorted_base_mapping_uninit.len());
906            sorted_base_mapping_init[offset as usize] = true;
907            sorted_base_mapping_uninit[offset as usize].write((*k, *v));
908        }
909        let final_sorted_base_mapping: Vec<(PageID, usize)> = if !config.cache_only {
910            assert_eq!(
911                base_mapping_unique.len(),
912                sorted_base_mapping_init.iter().filter(|&&b| b).count()
913            );
914            unsafe {
915                std::mem::transmute::<
916                    std::vec::Vec<std::mem::MaybeUninit<(PageID, usize)>>,
917                    std::vec::Vec<(PageID, usize)>,
918                >(sorted_base_mapping_uninit)
919            }
920        } else {
921            Vec::new()
922        };
923
924        // Finalize mini-page leaf node mappings of the snapshot
925        let mut final_mini_mapping: Vec<(PageID, usize)> = Vec::new();
926        for (k, v) in mini_mapping_unique.into_iter() {
927            final_mini_mapping.push((k, v));
928        }
929
930        let mut final_mini_size_mapping: Vec<(PageID, usize)> = Vec::new();
931        for (k, v) in mini_size_mapping_unique.into_iter() {
932            final_mini_size_mapping.push((k, v));
933        }
934
935        let leaf_page_num = if config.cache_only {
936            // For a pagelocation that's NULL (evicted), we don't know its version
937            // As such, we assume they are of the snapshot version and should be included.
938            // A few slots in page table mnight be wasted due to false positive.
939            // TODO: Be precise which requires some more metadata. Ideas:
940            // 1) Count number of child leaf nodes in snapshotted inner nodes
941            // 2) Put version in Null PageLocation.
942            // 3) Compact the snapshot file
943            leaf_count_upper
944        } else {
945            assert!(leaf_count_upper >= final_sorted_base_mapping.len());
946            final_sorted_base_mapping.len()
947        };
948
949        let mut file_size = std::mem::size_of::<BfTreeMeta>() as u64;
950
951        // Flush various mappings into the snapshot file
952        let (inner_offset, inner_size) =
953            serialize_vec_to_disk(&final_inner_mapping, &self.vfs.read().unwrap());
954
955        if inner_offset != 0 {
956            file_size = (inner_offset + align_to_sector_size(inner_size)) as u64;
957        }
958
959        let (mini_offset, mini_size) =
960            serialize_vec_to_disk(&final_mini_mapping, &self.vfs.read().unwrap());
961
962        if mini_offset != 0 {
963            file_size = (mini_offset + align_to_sector_size(mini_size)) as u64;
964        }
965
966        let (mini_size_offset, mini_size_size) =
967            serialize_vec_to_disk(&final_mini_size_mapping, &self.vfs.read().unwrap());
968
969        if mini_size_offset != 0 {
970            file_size = (mini_size_offset + align_to_sector_size(mini_size_size)) as u64;
971        }
972
973        let (base_offset, base_size) =
974            serialize_vec_to_disk(&final_sorted_base_mapping, &self.vfs.read().unwrap());
975
976        if base_offset != 0 {
977            file_size = (base_offset + align_to_sector_size(base_size)) as u64;
978        }
979
980        // Write the header to the first disk page of the snapshot file
981        let metadata = BfTreeMeta {
982            magic_begin: *BF_TREE_MAGIC_BEGIN,
983            root_id: unsafe { PageID::from_raw(self.root_id.load(Ordering::Acquire)) },
984            inner_offset,
985            inner_size,
986            mini_offset,
987            mini_size,
988            mini_size_offset,
989            mini_size_size,
990            base_offset,
991            base_size,
992            file_size,
993            leaf_page_num,
994            snapshot_version,
995            cache_only: config.cache_only,
996            cb_size_byte: config.cb_size_byte,
997            read_promotion_rate: config.read_promotion_rate.load(Ordering::Relaxed),
998            scan_promotion_rate: config.scan_promotion_rate.load(Ordering::Relaxed),
999            cb_min_record_size: config.cb_min_record_size,
1000            cb_max_record_size: config.cb_max_record_size,
1001            leaf_page_size: config.leaf_page_size,
1002            cb_max_key_len: config.cb_max_key_len,
1003            max_fence_len: config.max_fence_len,
1004            cb_copy_on_access_ratio: config.cb_copy_on_access_ratio,
1005            read_record_cache: config.read_record_cache,
1006            max_mini_page_size: config.max_mini_page_size,
1007            mini_page_binary_search: config.mini_page_binary_search,
1008            write_load_full_page: config.write_load_full_page,
1009            magic_end: *BF_TREE_MAGIC_END,
1010        };
1011
1012        let vfs = self.vfs.read().unwrap();
1013        vfs.write(META_DATA_PAGE_OFFSET, metadata.as_slice());
1014        vfs.flush();
1015
1016        self.reset();
1017    }
1018
1019    /// Take a CPR snapshot of a Bf-Tree
1020    pub fn snapshot(&self, tree: &BfTree, snapshot_file_path: impl AsRef<Path>) {
1021        // Allowing only one active snapshot at a time.
1022        if self
1023            .snapshot_in_progress
1024            .compare_exchange(false, true, Ordering::AcqRel, Ordering::Relaxed)
1025            .is_err()
1026        {
1027            println!("Another snapshot is in progress, skipping this snapshot request.");
1028            return;
1029        }
1030
1031        // Create a vfs for the snapshot
1032        let mut vfs_guard = self.vfs.write().unwrap();
1033        let snapshot_vfs = make_vfs(&tree.config.snapshot_backend, snapshot_file_path);
1034        let old_vfs = std::mem::replace(&mut *vfs_guard, snapshot_vfs);
1035        drop(old_vfs);
1036
1037        // Reset the snapshot vfs before use
1038        vfs_guard.reset();
1039
1040        // Drop the guard
1041        drop(vfs_guard);
1042
1043        // Initialize a inner node and leaf node mapping for the sweep
1044        let mut sweep_inner_mapping: Vec<(*const InnerNode, usize)> = Vec::new();
1045        let mut sweep_mini_mapping: Vec<(PageID, usize)> = Vec::new();
1046        let mut sweep_mini_size_mapping: Vec<(PageID, usize)> = Vec::new();
1047        let mut sweep_base_mapping: Vec<(PageID, usize)> = Vec::new();
1048
1049        // At the beginning, the global phase id must be 0 (REST).
1050        let mut current_global_phase_id = self.get_global_phase_id();
1051        assert_eq!(current_global_phase_id, PhaseId::Rest);
1052
1053        // Version of the ongoing snapshot is set
1054        let snapshot_version = self.get_global_version();
1055
1056        // Immediately move the global state to (1 (PREPARE), snapshot_version)
1057        let mut current_global_state = self.advance_global_state();
1058        current_global_phase_id = self.get_global_phase_id();
1059        assert_eq!(current_global_phase_id, PhaseId::Prepare);
1060        assert_eq!(snapshot_version, self.get_global_version());
1061
1062        let mut leaf_node_count_upper_bound = 0; // Indicate the total number of leaf nodes in the captured snapshot.
1063
1064        loop {
1065            if self.check_if_phase_completed(current_global_state) {
1066                match current_global_phase_id {
1067                    PhaseId::Rest => {
1068                        // Upon reaching here, all user threads are in (REST, snapshot_version + 1).
1069                        // All snapshots of pages of snapshot_version are done, and no more snapshot operations
1070                        // neither. As such, we can safely finalize the snapshot by writing out the
1071                        // metadata and page mappings.
1072                        self.finalize(
1073                            snapshot_version,
1074                            &mut sweep_inner_mapping,
1075                            &mut sweep_mini_mapping,
1076                            &mut sweep_mini_size_mapping,
1077                            &mut sweep_base_mapping,
1078                            leaf_node_count_upper_bound,
1079                            tree.config.clone(),
1080                        );
1081
1082                        // Close the snapshot vfs
1083                        let mut vfs_guard = self.vfs.write().unwrap();
1084                        let snapshot_vfs = make_vfs(&StorageBackend::Memory, ":memory:");
1085                        let old_vfs = std::mem::replace(&mut *vfs_guard, snapshot_vfs);
1086                        drop(old_vfs);
1087                        drop(vfs_guard);
1088
1089                        // The snapshot is done.
1090                        break;
1091                    }
1092                    PhaseId::Prepare => {
1093                        current_global_state = self.advance_global_state();
1094                        current_global_phase_id = self.get_global_phase_id();
1095                        assert_eq!(current_global_phase_id, PhaseId::InProgress);
1096                        assert_eq!(snapshot_version + 1, self.get_global_version());
1097                    }
1098                    PhaseId::InProgress => {
1099                        current_global_state = self.advance_global_state();
1100                        current_global_phase_id = self.get_global_phase_id();
1101                        assert_eq!(current_global_phase_id, PhaseId::Sweep);
1102                        assert_eq!(snapshot_version + 1, self.get_global_version());
1103                    }
1104                    PhaseId::Sweep => {
1105                        // Upon reaching here, there are no user threads with snapshot_version anymore.
1106                        // Sweep through and snapshot all data pages whose version is less than (snapshot_version + 1)
1107                        // and build inner node and leaf node mapping for those pages.
1108                        // Upon completion, all pages with version less than (snapshot_version + 1) should have been captured in the snapshot.
1109                        // Note that, there could be duplicate snapshots during and after the sweep as user threads in 'SWEEPING'
1110                        // state keeps taking snapshots of pages even if they have been captured by the sweep as the sweep does not
1111                        // alter page versions. De-duplication is needed when finalizing the snapshot file.
1112                        leaf_node_count_upper_bound = self.sweep(
1113                            tree,
1114                            snapshot_version + 1,
1115                            &mut sweep_inner_mapping,
1116                            &mut sweep_mini_mapping,
1117                            &mut sweep_mini_size_mapping,
1118                            &mut sweep_base_mapping,
1119                        );
1120
1121                        current_global_state = self.advance_global_state();
1122                        current_global_phase_id = self.get_global_phase_id();
1123                        assert_eq!(current_global_phase_id, PhaseId::Rest);
1124                        assert_eq!(snapshot_version + 1, self.get_global_version());
1125                    }
1126                }
1127            }
1128
1129            // Wait for all threads to complete phase transition.
1130            // Use atomic wait instead of polling with fixed sleep.
1131            self.wait_for_phase_completion(current_global_state);
1132
1133            #[cfg(all(feature = "shuttle", test))]
1134            shuttle::thread::yield_now();
1135        }
1136
1137        assert!(self
1138            .snapshot_in_progress
1139            .compare_exchange(true, false, Ordering::AcqRel, Ordering::Relaxed)
1140            .is_ok());
1141    }
1142
1143    /// Recover a Bf-tree from an existing snapshot file at recovery_snapshot_file_path.
1144    ///
1145    /// Most configuration params are directly retrieved from the snapshot file
1146    /// with the following ones for the caller to specify:
1147    /// 1. use_snapshot: whether the new tree will also support snapshot.
1148    /// 2. new_snapshot_file_path: If use_snapshot, then provide the path of the snapshot file of the newly recovered Bf-tree.
1149    ///    Note that, this path must be different from the recovery_snapshot_file_path.
1150    /// 3. buffer_ptr: optional pointer to a pre-allocated buffer for the newly recovered Bf-tree
1151    /// 4. buffer_size optional override of the buffer size retrieved from the snapshot file.
1152    ///    Note that, if the newly specified value is smaller than the one retrieved from the snapshot file,
1153    ///    then failure to restore a tree might happen. This is because during recovery, the memory cache pages
1154    ///    of the tree at the moment of snapshot are reinstated into memory too.
1155    /// 5. wal_config: optional write-ahead log configuration for the newly recovered Bf-tree
1156    pub fn new_from_snapshot(
1157        recovery_snapshot_file_path: impl AsRef<Path>, //  The snapshot file to recover from
1158        use_snapshot: bool,
1159        buffer_ptr: Option<*mut u8>,
1160        buffer_size: Option<usize>, // buffer size of the newly created Bf-tree
1161        wal_config: Option<Arc<WalConfig>>,
1162    ) -> Result<BfTree, ConfigError> {
1163        // Check the recovery file is valid
1164        if !recovery_snapshot_file_path.as_ref().exists() {
1165            // if not already exist, we just create a new empty file at the location.
1166            return Err(ConfigError::SnapshotFileInvalid(
1167                "Not found ".to_string() + recovery_snapshot_file_path.as_ref().to_str().unwrap(),
1168            ));
1169        }
1170
1171        // Create WAL, if specified
1172        let wal = wal_config.as_ref().map(|s| WriteAheadLog::new(s.clone()));
1173
1174        // Retrieve the header of the snapshot file and construct a valid config for the to-be-recovered Bf-tree
1175        let reader = std::fs::File::open(recovery_snapshot_file_path.as_ref()).unwrap();
1176        let mut metadata = SectorAlignedVector::new_zeroed(DISK_PAGE_SIZE); // Metadata is at most one disk page in size
1177        #[cfg(unix)]
1178        {
1179            reader.read_at(&mut metadata, 0).unwrap();
1180        }
1181        #[cfg(windows)]
1182        {
1183            reader.seek_read(&mut metadata, 0).unwrap();
1184        }
1185
1186        let bf_meta = unsafe { (metadata.as_ptr() as *const BfTreeMeta).read() };
1187        bf_meta.check_magic();
1188        assert_eq!(reader.metadata().unwrap().len(), bf_meta.file_size);
1189
1190        let mut bf_tree_config = Config::new_from_snapshot(&bf_meta);
1191
1192        let recovery_snapshot_file_backend = StorageBackend::Std; // TODO, recover storage backend from snapshot file
1193        if !bf_tree_config.cache_only {
1194            bf_tree_config.file_path(recovery_snapshot_file_path.as_ref());
1195            // The storage backend should use the same vfs system as the recovery snapshot file
1196            bf_tree_config.storage_backend = recovery_snapshot_file_backend.clone();
1197        } else {
1198            bf_tree_config.storage_backend = StorageBackend::Memory;
1199        }
1200        bf_tree_config.use_snapshot = use_snapshot;
1201
1202        bf_tree_config.snapshot_backend = StorageBackend::Std; // TODO, allow user chosen snapshot backend
1203
1204        let snapshot_mgr = if bf_tree_config.use_snapshot {
1205            Some(Arc::new(CPRSnapShotMgr::new(
1206                bf_tree_config.snapshot_version,
1207            )))
1208        } else {
1209            None
1210        };
1211
1212        if let Some(size) = buffer_size {
1213            bf_tree_config.cb_size_byte = size
1214        }
1215
1216        let size_classes = BfTree::create_mem_page_size_classes(
1217            bf_tree_config.cb_min_record_size,
1218            bf_tree_config.cb_max_record_size,
1219            bf_tree_config.leaf_page_size,
1220            bf_tree_config.max_fence_len,
1221            bf_tree_config.cache_only,
1222        );
1223
1224        bf_tree_config.write_ahead_log = wal_config.clone();
1225        bf_tree_config.validate()?;
1226
1227        let config = Arc::new(bf_tree_config);
1228
1229        // Start Bf-Tree re-construction using recovery_snapshot_file_path and the config
1230        let recovery_snapshot_vfs = make_vfs(
1231            &recovery_snapshot_file_backend,
1232            recovery_snapshot_file_path.as_ref(),
1233        );
1234
1235        // Step 1: reconstruct inner nodes.
1236        let mut root_page_id = bf_meta.root_id;
1237        let mut inner_node_page_buffer = SectorAlignedVector::new_zeroed(INNER_NODE_SIZE);
1238        if root_page_id.is_inner_node_pointer() {
1239            let inner_mapping: Vec<(*const InnerNode, usize)> = read_vec_from_offset(
1240                bf_meta.inner_offset,
1241                bf_meta.inner_size,
1242                &recovery_snapshot_vfs,
1243            );
1244
1245            // Sanity check on root nodes
1246            let mut root_cnt = 0;
1247            for (_ptr, offset) in &inner_mapping {
1248                recovery_snapshot_vfs.read(*offset, &mut inner_node_page_buffer);
1249                let inner_node = InnerNodeBuilder::new().build_from_slice(&inner_node_page_buffer);
1250                if unsafe { (*inner_node).is_root() } {
1251                    root_cnt += 1;
1252                }
1253                InnerNode::free_node(inner_node);
1254            }
1255            assert_eq!(root_cnt, 1, "Root count in inner mapping: {}", root_cnt);
1256
1257            let mut inner_map = HashMap::new();
1258
1259            for m in inner_mapping {
1260                inner_map.insert(m.0, m.1);
1261            }
1262            let offset = inner_map.get(&root_page_id.as_inner_node()).unwrap();
1263            recovery_snapshot_vfs.read(*offset, &mut inner_node_page_buffer);
1264            let root_page = InnerNodeBuilder::new().build_from_slice(&inner_node_page_buffer);
1265
1266            // No need for disk offset of a inner node.
1267            unsafe {
1268                (*root_page).set_disk_offset(INVALID_DISK_OFFSET as u64);
1269            }
1270            // Mark the root node
1271            unsafe {
1272                (*root_page).set_root(true);
1273            }
1274            root_page_id = PageID::from_pointer(root_page);
1275
1276            let mut inner_resolve_queue = VecDeque::from([root_page]);
1277            while !inner_resolve_queue.is_empty() {
1278                let inner_ptr = inner_resolve_queue.pop_front().unwrap();
1279                let mut inner = ReadGuard::try_read(inner_ptr).unwrap().upgrade().unwrap();
1280                if inner.as_ref().meta.children_is_leaf() {
1281                    continue;
1282                }
1283                for (idx, c) in inner.as_ref().get_child_iter().enumerate() {
1284                    let offset = inner_map.get(&c.as_inner_node()).unwrap();
1285                    recovery_snapshot_vfs.read(*offset, &mut inner_node_page_buffer);
1286                    let inner_page =
1287                        InnerNodeBuilder::new().build_from_slice(&inner_node_page_buffer);
1288                    unsafe {
1289                        (*inner_page).set_disk_offset(INVALID_DISK_OFFSET as u64);
1290                    }
1291                    let inner_id = PageID::from_pointer(inner_page);
1292                    inner.as_mut().update_at_pos(idx, inner_id);
1293                    inner_resolve_queue.push_back(inner_page);
1294                }
1295            }
1296        }
1297
1298        let raw_root_id = if root_page_id.is_id() {
1299            root_page_id.raw() | BfTree::ROOT_IS_LEAF_MASK
1300        } else {
1301            root_page_id.raw()
1302        };
1303
1304        // Step 2. Reconstruct the page table and leaf pages.
1305        // Here we differentiate cache-only from disk-backed Bf-trees as their recovery process differs.
1306        if !bf_meta.cache_only {
1307            // For disk backed bf-tree, we reconstruct the page table using base pages.
1308            let base_mapping: Vec<(PageID, usize)> = if bf_meta.base_size > 0 {
1309                read_vec_from_offset(
1310                    bf_meta.base_offset,
1311                    bf_meta.base_size,
1312                    &recovery_snapshot_vfs,
1313                )
1314            } else {
1315                Vec::new()
1316            };
1317
1318            let base_page_loc_mapping = base_mapping.into_iter().map(|(pid, offset)| {
1319                let loc = PageLocation::Base(offset);
1320                (pid, loc)
1321            });
1322
1323            // The file system of the newly constructed Bf-tree is the recovery_snapshot_file
1324            let pt = PageTable::new_from_mapping(
1325                base_page_loc_mapping,
1326                recovery_snapshot_vfs.clone(),
1327                config.clone(),
1328                snapshot_mgr.clone(),
1329            );
1330
1331            let circular_buffer = CircularBuffer::new(
1332                config.cb_size_byte,
1333                config.cb_copy_on_access_ratio,
1334                config.cb_min_record_size,
1335                config.cb_max_record_size,
1336                config.leaf_page_size,
1337                config.max_fence_len,
1338                buffer_ptr,
1339                config.cache_only,
1340            );
1341
1342            // Next, we restore the mini-pages and wire them to the corresponding base pages and update the page table.
1343            let mini_mapping: Vec<(PageID, usize)> = if bf_meta.mini_size > 0 {
1344                read_vec_from_offset(
1345                    bf_meta.mini_offset,
1346                    bf_meta.mini_size,
1347                    &recovery_snapshot_vfs,
1348                )
1349            } else {
1350                Vec::new()
1351            };
1352
1353            let mini_size_mapping: Vec<(PageID, usize)> = if bf_meta.mini_size_size > 0 {
1354                read_vec_from_offset(
1355                    bf_meta.mini_size_offset,
1356                    bf_meta.mini_size_size,
1357                    &recovery_snapshot_vfs,
1358                )
1359            } else {
1360                Vec::new()
1361            };
1362
1363            let mut mini_size_mapping_unique: HashMap<PageID, usize> = HashMap::new();
1364            for (pid, size) in mini_size_mapping {
1365                mini_size_mapping_unique.insert(pid, size);
1366            }
1367
1368            // Create the storage system first before allocating mini-pages.
1369            let storage =
1370                LeafStorage::new_inner(config.clone(), pt, circular_buffer, recovery_snapshot_vfs);
1371
1372            for (pid, offset) in &mini_mapping {
1373                let mini_size = *mini_size_mapping_unique.get(pid).unwrap();
1374
1375                // Allocate space in memory for new mini-page
1376                let mini_page_guard = match storage.alloc_mini_page(mini_size) {
1377                    Ok(mini_page_ptr) => mini_page_ptr,
1378                    Err(_) => {
1379                        return Err(ConfigError::CircularBufferSize("buffer size set too small. Consider increasing it or not specifying at all".to_string()));
1380                    }
1381                };
1382
1383                // Copy mini-page from snapshot file to the newly allocated mini-page
1384                let mut page_buffer = SectorAlignedVector::new_zeroed(mini_size);
1385                storage.vfs.read(*offset, &mut page_buffer);
1386                unsafe {
1387                    std::ptr::copy_nonoverlapping(
1388                        page_buffer.as_ptr(),
1389                        mini_page_guard.as_ptr(),
1390                        mini_size,
1391                    );
1392                }
1393
1394                // Connect the new mini-page to the corresponding base page in page table
1395                let new_mini_ptr = mini_page_guard.as_ptr() as *mut LeafNode;
1396                let mini_page = unsafe { &mut *new_mini_ptr };
1397
1398                let mut base_page = storage.page_table.get_mut(pid);
1399                let page_loc = base_page.get_page_location().clone();
1400                match page_loc {
1401                    PageLocation::Base(off) => {
1402                        mini_page.next_level = MiniPageNextLevel::new(off);
1403                    }
1404                    _ => {
1405                        panic!("Unexpected page location for base page");
1406                    }
1407                }
1408                let mini_loc = PageLocation::Mini(new_mini_ptr);
1409
1410                // Replace the base page with the mini-page in the page table
1411                base_page.create_cache_page_loc(mini_loc);
1412            }
1413
1414            Ok(BfTree {
1415                storage,
1416                root_page_id: AtomicU64::new(raw_root_id),
1417                wal,
1418                write_load_full_page: config.write_load_full_page,
1419                cache_only: false,
1420                mini_page_size_classes: size_classes,
1421                snapshot_mgr,
1422                config,
1423                #[cfg(any(feature = "metrics-rt-debug-all", feature = "metrics-rt-debug-timer"))]
1424                metrics_recorder: Some(Arc::new(ThreadLocal::new())),
1425            })
1426        } else {
1427            // For cache-only mode, we create a new page table with NULL pages
1428            let mini_mapping_unallocated: Vec<(PageID, PageLocation)> = (0..bf_meta.leaf_page_num)
1429                .map(|pid| (PageID::from_id(pid as u64), PageLocation::Null))
1430                .collect();
1431
1432            // Then we restore the mini-pages and replace the corresponding Null pages and update the page table.
1433            let mini_mapping: Vec<(PageID, usize)> = if bf_meta.mini_size > 0 {
1434                read_vec_from_offset(
1435                    bf_meta.mini_offset,
1436                    bf_meta.mini_size,
1437                    &recovery_snapshot_vfs,
1438                )
1439            } else {
1440                Vec::new()
1441            };
1442
1443            let mini_size_mapping: Vec<(PageID, usize)> = if bf_meta.mini_size_size > 0 {
1444                read_vec_from_offset(
1445                    bf_meta.mini_size_offset,
1446                    bf_meta.mini_size_size,
1447                    &recovery_snapshot_vfs,
1448                )
1449            } else {
1450                Vec::new()
1451            };
1452
1453            let mut mini_size_mapping_unique: HashMap<PageID, usize> = HashMap::new();
1454            for (pid, size) in mini_size_mapping {
1455                mini_size_mapping_unique.insert(pid, size);
1456            }
1457
1458            // For cache-only mode, the file system of the new bf-tree is memory-based
1459            let storage_vfs = make_vfs(&config.storage_backend, PathBuf::new());
1460
1461            let pt = PageTable::new_from_mapping(
1462                mini_mapping_unallocated.into_iter(),
1463                storage_vfs.clone(),
1464                config.clone(),
1465                snapshot_mgr.clone(),
1466            );
1467
1468            let circular_buffer = CircularBuffer::new(
1469                config.cb_size_byte,
1470                config.cb_copy_on_access_ratio,
1471                config.cb_min_record_size,
1472                config.cb_max_record_size,
1473                config.leaf_page_size,
1474                config.max_fence_len,
1475                buffer_ptr,
1476                config.cache_only,
1477            );
1478
1479            // Create a memory-based storage system before allocating mini-pages.
1480            let storage = LeafStorage::new_inner(config.clone(), pt, circular_buffer, storage_vfs);
1481
1482            for (pid, offset) in &mini_mapping {
1483                let mini_size = *mini_size_mapping_unique.get(pid).unwrap();
1484
1485                // Skip over null pages as the default is Null in the page table already
1486                if *offset == NULL_PAGE_LOCATION_OFFSET {
1487                    continue;
1488                }
1489
1490                // Allocate memory for a new mini-page in storage
1491                let mini_page_guard = match storage.alloc_mini_page(mini_size) {
1492                    Ok(mini_page_ptr) => mini_page_ptr,
1493                    Err(_) => {
1494                        panic!("Please increase cb_size_byte in config");
1495                    }
1496                };
1497
1498                // Copy mini-page from snapshot file to the newly allocated space.
1499                let mut page_buffer = SectorAlignedVector::new_zeroed(mini_size);
1500                recovery_snapshot_vfs.read(*offset, &mut page_buffer);
1501                unsafe {
1502                    std::ptr::copy_nonoverlapping(
1503                        page_buffer.as_ptr(),
1504                        mini_page_guard.as_ptr(),
1505                        mini_size,
1506                    );
1507                }
1508
1509                // Set its next level to oblivion
1510                let mini_page_ptr = mini_page_guard.as_ptr() as *mut LeafNode;
1511                let mini_page = unsafe { &mut *mini_page_ptr };
1512                mini_page.next_level = MiniPageNextLevel::new_null();
1513
1514                // Update the corresponding page location
1515                let mut null_page = storage.page_table.get_mut(pid);
1516                let page_loc = null_page.get_page_location().clone();
1517                match page_loc {
1518                    PageLocation::Null => {
1519                        let mini_loc = PageLocation::Mini(mini_page_ptr);
1520                        null_page.create_cache_page_loc(mini_loc);
1521                    }
1522                    _ => {
1523                        panic!("Unexpected page location for null page");
1524                    }
1525                }
1526            }
1527            Ok(BfTree {
1528                storage,
1529                root_page_id: AtomicU64::new(raw_root_id),
1530                wal,
1531                write_load_full_page: config.write_load_full_page,
1532                cache_only: true,
1533                mini_page_size_classes: size_classes,
1534                snapshot_mgr,
1535                config,
1536                #[cfg(any(feature = "metrics-rt-debug-all", feature = "metrics-rt-debug-timer"))]
1537                metrics_recorder: Some(Arc::new(ThreadLocal::new())),
1538            })
1539        }
1540    }
1541}
1542
1543impl BfTree {
1544    /// Recovery a Bf-Tree from a cpr snapshot and WAL files.
1545    /// Incomplete function, internal use only
1546    pub fn recovery(
1547        recovery_snapshot_file_path: PathBuf, //  The snapshot file to recover from
1548        wal_file: impl AsRef<Path>,
1549        use_snapshot: bool,
1550        buffer_ptr: Option<*mut u8>,
1551        buffer_size: Option<usize>,
1552        wal: Option<Arc<WalConfig>>,
1553    ) {
1554        let bf_tree = BfTree::new_from_cpr_snapshot(
1555            recovery_snapshot_file_path,
1556            use_snapshot,
1557            buffer_ptr,
1558            buffer_size,
1559            wal,
1560        )
1561        .unwrap();
1562        let wal_reader = WalReader::new(wal_file, 4096);
1563
1564        for seg in wal_reader.segment_iter() {
1565            for entry in seg.entry_iter() {
1566                let log_entry = LogEntry::read_from_buffer(entry.1);
1567                match log_entry {
1568                    LogEntry::Write(op) => {
1569                        bf_tree.insert(op.key, op.value);
1570                    }
1571                    LogEntry::Split(_op) => {
1572                        todo!("implement split op in wal!")
1573                    }
1574                }
1575            }
1576        }
1577    }
1578
1579    /// Take a new CPR snapshot
1580    pub fn cpr_snapshot(&self, snapshot_file_path: impl AsRef<Path>) {
1581        if !self.config.use_snapshot {
1582            panic!("Snapshots are not enabled in the configuration");
1583        }
1584
1585        let snpshot_mgr = self.snapshot_mgr.clone().unwrap();
1586        snpshot_mgr.snapshot(self, snapshot_file_path);
1587    }
1588
1589    /// Recover a BfTree from a CPR snapshot
1590    pub fn new_from_cpr_snapshot(
1591        recovery_snapshot_file_path: impl AsRef<Path>, //  The snapshot file to recover from
1592        use_snapshot: bool,
1593        buffer_ptr: Option<*mut u8>,
1594        buffer_size: Option<usize>,
1595        wal: Option<Arc<WalConfig>>,
1596    ) -> Result<BfTree, ConfigError> {
1597        CPRSnapShotMgr::new_from_snapshot(
1598            recovery_snapshot_file_path,
1599            use_snapshot,
1600            buffer_ptr,
1601            buffer_size,
1602            wal,
1603        )
1604    }
1605
1606    /// Check if all threads are running in the next version of the current snapshot
1607    pub fn are_all_threads_in_next_snapshot_version(&self) -> bool {
1608        if let Some(snapshot_mgr) = &self.snapshot_mgr {
1609            return snapshot_mgr.are_all_threads_in_next_version();
1610        }
1611        false
1612    }
1613}
1614
1615struct SectorAlignedVector {
1616    inner: ManuallyDrop<Vec<u8>>,
1617}
1618
1619impl Drop for SectorAlignedVector {
1620    fn drop(&mut self) {
1621        let layout =
1622            std::alloc::Layout::from_size_align(self.inner.capacity(), SECTOR_SIZE).unwrap();
1623        let ptr = self.inner.as_mut_ptr();
1624        unsafe {
1625            std::alloc::dealloc(ptr, layout);
1626        }
1627    }
1628}
1629
1630impl SectorAlignedVector {
1631    fn new_zeroed(capacity: usize) -> Self {
1632        let layout = std::alloc::Layout::from_size_align(capacity, SECTOR_SIZE).unwrap();
1633        let ptr = unsafe { std::alloc::alloc_zeroed(layout) };
1634
1635        let inner = unsafe { Vec::from_raw_parts(ptr, capacity, capacity) };
1636        Self {
1637            inner: ManuallyDrop::new(inner),
1638        }
1639    }
1640}
1641
1642impl Deref for SectorAlignedVector {
1643    type Target = Vec<u8>;
1644
1645    fn deref(&self) -> &Self::Target {
1646        &self.inner
1647    }
1648}
1649
1650impl DerefMut for SectorAlignedVector {
1651    fn deref_mut(&mut self) -> &mut Self::Target {
1652        &mut self.inner
1653    }
1654}
1655
1656/// We use repr(C) for simplicity, maybe flatbuffer or bincode or even repr(Rust) is better.
1657/// But we don't care about the space here.
1658/// I don't want to introduce giant dependencies just for this.
1659#[repr(C, align(512))]
1660pub(crate) struct BfTreeMeta {
1661    magic_begin: [u8; 16],
1662    // Snapshot file metadata
1663    root_id: PageID,
1664    inner_offset: usize,
1665    inner_size: usize,
1666    mini_offset: usize,
1667    mini_size: usize,
1668    mini_size_offset: usize,
1669    mini_size_size: usize,
1670    base_offset: usize,
1671    base_size: usize,
1672    file_size: u64,
1673    leaf_page_num: usize,
1674    // Bf-tree configuration of the snapshot
1675    pub(crate) cb_size_byte: usize,
1676    pub(crate) snapshot_version: u64,
1677    pub(crate) cache_only: bool,
1678    pub(crate) read_promotion_rate: usize,
1679    pub(crate) scan_promotion_rate: usize,
1680    pub(crate) cb_min_record_size: usize,
1681    pub(crate) cb_max_record_size: usize,
1682    pub(crate) leaf_page_size: usize,
1683    pub(crate) cb_max_key_len: usize,
1684    pub(crate) max_fence_len: usize,
1685    pub(crate) cb_copy_on_access_ratio: f64,
1686    pub(crate) read_record_cache: bool,
1687    pub(crate) max_mini_page_size: usize,
1688    pub(crate) mini_page_binary_search: bool,
1689    pub(crate) write_load_full_page: bool,
1690    magic_end: [u8; 14],
1691}
1692const _: () = assert!(std::mem::size_of::<BfTreeMeta>() <= DISK_PAGE_SIZE);
1693
1694impl BfTreeMeta {
1695    fn as_slice(&self) -> &[u8] {
1696        let ptr = self as *const Self as *const u8;
1697        let size = std::mem::size_of::<Self>();
1698        unsafe { std::slice::from_raw_parts(ptr, size) }
1699    }
1700
1701    fn check_magic(&self) {
1702        assert_eq!(self.magic_begin, *BF_TREE_MAGIC_BEGIN);
1703        assert_eq!(self.magic_end, *BF_TREE_MAGIC_END);
1704    }
1705}
1706
1707/// Returns starting offset and total size written to disk.
1708fn serialize_vec_to_disk<T>(v: &[T], vfs: &Arc<dyn VfsImpl>) -> (usize, usize) {
1709    if v.is_empty() {
1710        return (0, 0);
1711    }
1712    let unaligned_ptr = v.as_ptr() as *const u8;
1713    let unaligned_size = std::mem::size_of_val(v);
1714
1715    let aligned_size = align_to_sector_size(unaligned_size);
1716    let layout = std::alloc::Layout::from_size_align(aligned_size, SECTOR_SIZE).unwrap();
1717    unsafe {
1718        let aligned_ptr = std::alloc::alloc_zeroed(layout);
1719        std::ptr::copy_nonoverlapping(unaligned_ptr, aligned_ptr, unaligned_size);
1720        let slice = std::slice::from_raw_parts(aligned_ptr, aligned_size);
1721        let offset = serialize_u8_slice_to_disk(slice, vfs);
1722        std::alloc::dealloc(aligned_ptr, layout);
1723        (offset, unaligned_size)
1724    }
1725}
1726
1727fn read_vec_from_offset<T: Clone>(offset: usize, size: usize, vfs: &Arc<dyn VfsImpl>) -> Vec<T> {
1728    assert!(size > 0);
1729    let slice = read_u8_slice_from_disk(offset, size, vfs);
1730    let ptr = slice.as_ptr() as *const T;
1731    let size = size / std::mem::size_of::<T>();
1732    let slice = unsafe { std::slice::from_raw_parts(ptr, size) };
1733    slice.to_vec()
1734}
1735
1736fn read_u8_slice_from_disk(offset: usize, size: usize, vfs: &Arc<dyn VfsImpl>) -> Vec<u8> {
1737    let mut res = Vec::new();
1738    let mut buffer = vec![0; DISK_PAGE_SIZE];
1739    for i in (0..size).step_by(DISK_PAGE_SIZE) {
1740        vfs.read(offset + i, &mut buffer); // Read one disk page at a time
1741        res.extend_from_slice(&buffer);
1742    }
1743    res
1744}
1745
1746const SECTOR_SIZE: usize = 512;
1747
1748fn align_to_sector_size(n: usize) -> usize {
1749    (n + SECTOR_SIZE - 1) & !(SECTOR_SIZE - 1)
1750}
1751
1752/// Write a slice to disk and return the start offset and page count.
1753/// TODO: we should not just return offset and count, because the offset is not necessarily continuos.
1754///     We should return a Vec of offsets. But let's keep it simple for fast prototype.
1755fn serialize_u8_slice_to_disk(slice: &[u8], vfs: &Arc<dyn VfsImpl>) -> usize {
1756    let mut start_offset = None;
1757    for chunk in slice.chunks(DISK_PAGE_SIZE) {
1758        let offset = vfs.alloc_offset(DISK_PAGE_SIZE); // Write one disk page at a time
1759        if start_offset.is_none() {
1760            start_offset = Some(offset);
1761        }
1762        vfs.write(offset, chunk);
1763    }
1764    start_offset.unwrap()
1765}
1766
1767#[cfg(test)]
1768mod tests {
1769    use crate::{nodes::leaf_node::LeafReadResult, sync::thread, BfTree, Config};
1770    use std::panic;
1771    #[cfg(feature = "shuttle")]
1772    use std::path::PathBuf;
1773    use std::str::FromStr;
1774    use std::sync::atomic::Ordering;
1775    use std::sync::{atomic::AtomicBool, Arc};
1776
1777    /// Multiple writer threads write to a BfTree in parallel while a separate thread taking multiple snapshots
1778    /// A new BfTree recovered from the snapshot should contain a prefix of all the inserts from each writer thread.
1779    /// A snapshot taken later should cover the previous snapshots.
1780    #[test]
1781    fn cpr_snapshot_disk() {
1782        // Install a panic hook that triggers the just-in-time debugger (e.g. VS debugger)
1783        // so we can inspect the state at the point of failure.
1784        panic::set_hook(Box::new(|info| {
1785            eprintln!("PANIC: {info}");
1786            unsafe { std::arch::asm!("int 3") };
1787        }));
1788
1789        let min_record_size: usize = 64;
1790        let max_record_size: usize = 2408;
1791        let leaf_page_size: usize = 8192;
1792        let snapshot_num: usize = 10;
1793        let num_threads: usize = 4;
1794        let file_path: String = "target/test_simple.bftree".to_string();
1795        let snapshot_file_path: String = "target/test_simple_snapshot.bftree".to_string();
1796
1797        let tmp_file_path = std::path::PathBuf::from_str(&file_path).unwrap();
1798        let tmp_snapshot_file_path = std::path::PathBuf::from_str(&snapshot_file_path).unwrap();
1799
1800        let mut config = Config::new(&tmp_file_path, 128 * 1024); // 128KB buffer pool. insert/split/eviction all triggered
1801        config.storage_backend(crate::StorageBackend::Std);
1802        config.cb_min_record_size = min_record_size + 2 * std::mem::size_of::<usize>();
1803        config.cb_max_record_size = max_record_size;
1804        config.leaf_page_size = leaf_page_size;
1805        config.max_fence_len = min_record_size + 2 * std::mem::size_of::<usize>();
1806        config.use_snapshot(true);
1807
1808        let bftree = Arc::new(BfTree::with_config(config.clone(), None).unwrap());
1809        let finish = Arc::new(AtomicBool::new(false));
1810
1811        let handles: Vec<_> = (0..num_threads)
1812            .map(|i| {
1813                let finish_clone = finish.clone();
1814                let bftree_clone = bftree.clone();
1815
1816                thread::spawn(move || {
1817                    let key_len: usize = min_record_size / 2 + std::mem::size_of::<usize>();
1818                    assert!(key_len * 2 <= max_record_size);
1819                    let mut key_buffer = vec![0usize; key_len / std::mem::size_of::<usize>()];
1820
1821                    let mut r: usize = 0;
1822                    while !finish_clone.load(Ordering::Relaxed) {
1823                        key_buffer.fill(r);
1824                        key_buffer[0] = i;
1825
1826                        match bftree_clone.insert(
1827                            bytemuck::must_cast_slice::<usize, u8>(&key_buffer),
1828                            bytemuck::must_cast_slice::<usize, u8>(&key_buffer),
1829                        ) {
1830                            crate::LeafInsertResult::Success => {}
1831                            _ => {
1832                                panic!("Insert failed");
1833                            }
1834                        }
1835                        r += 1;
1836                    }
1837                    r
1838                })
1839            })
1840            .collect();
1841
1842        thread::sleep(std::time::Duration::from_secs(5));
1843        for _ in 0..snapshot_num {
1844            // take a snapshot
1845            let _ = std::fs::remove_file(&tmp_snapshot_file_path);
1846            bftree.cpr_snapshot(&tmp_snapshot_file_path);
1847            thread::sleep(std::time::Duration::from_secs(5));
1848        }
1849
1850        // Stop all writer threads
1851        let mut rs = vec![0usize; num_threads];
1852        finish.store(true, Ordering::Relaxed);
1853        for (i, h) in handles.into_iter().enumerate() {
1854            let r = h.join().unwrap();
1855            rs[i] = r;
1856        }
1857
1858        verify_snapshot_recovery(
1859            &tmp_snapshot_file_path,
1860            num_threads,
1861            min_record_size,
1862            &rs,
1863            true,
1864        );
1865
1866        std::fs::remove_file(tmp_file_path).unwrap();
1867        std::fs::remove_file(tmp_snapshot_file_path).unwrap();
1868    }
1869
1870    /// Testing snapshot for cache-only mode with std::thread
1871    #[test]
1872    fn cpr_snapshot_cache_only() {
1873        let min_record_size: usize = 64;
1874        let max_record_size: usize = 2408;
1875        let leaf_page_size: usize = 8192;
1876        let num_threads: usize = 4;
1877
1878        let snapshot_file_path: String =
1879            "target/test_simple_cache_only_snapshot.bftree".to_string();
1880        let tmp_snapshot_file_path = std::path::PathBuf::from_str(&snapshot_file_path).unwrap();
1881
1882        let mut config = Config::default(); // Creat a CB that can hold 16 full pages
1883        config.storage_backend(crate::StorageBackend::Memory);
1884        config.file_path(":memory:");
1885        config.cache_only = true;
1886        config.cb_size_byte(1024 * 1024 * 1024);
1887        config.cb_min_record_size = min_record_size;
1888        config.cb_max_record_size = max_record_size;
1889        config.leaf_page_size = leaf_page_size;
1890        config.max_fence_len = max_record_size;
1891        config.use_snapshot(true);
1892
1893        let bftree = Arc::new(BfTree::with_config(config.clone(), None).unwrap());
1894        let finish = Arc::new(AtomicBool::new(false));
1895
1896        let handles: Vec<_> = (0..num_threads)
1897            .map(|i| {
1898                let finish_clone = finish.clone();
1899                let bftree_clone = bftree.clone();
1900
1901                thread::spawn(move || {
1902                    let key_len: usize = min_record_size / 2 + std::mem::size_of::<usize>();
1903                    assert!(key_len * 2 <= max_record_size);
1904                    let mut key_buffer = vec![0usize; key_len / std::mem::size_of::<usize>()];
1905
1906                    let mut r: usize = 0;
1907                    while !finish_clone.load(Ordering::Relaxed) {
1908                        key_buffer.fill(r);
1909                        key_buffer[0] = i;
1910
1911                        match bftree_clone.insert(
1912                            bytemuck::must_cast_slice::<usize, u8>(&key_buffer),
1913                            bytemuck::must_cast_slice::<usize, u8>(&key_buffer),
1914                        ) {
1915                            crate::LeafInsertResult::Success => {}
1916                            _ => {
1917                                panic!("Insert failed");
1918                            }
1919                        }
1920                        r += 1;
1921                    }
1922                    r
1923                })
1924            })
1925            .collect();
1926
1927        thread::sleep(std::time::Duration::from_secs(5));
1928        // take a snapshot
1929        bftree.cpr_snapshot(&tmp_snapshot_file_path);
1930        thread::sleep(std::time::Duration::from_secs(5));
1931
1932        // Stop all writer threads
1933        let mut rs = vec![0usize; num_threads];
1934        finish.store(true, Ordering::Relaxed);
1935        for (i, h) in handles.into_iter().enumerate() {
1936            let r = h.join().unwrap();
1937            rs[i] = r;
1938        }
1939
1940        verify_snapshot_recovery(
1941            &tmp_snapshot_file_path,
1942            num_threads,
1943            min_record_size,
1944            &rs,
1945            false,
1946        );
1947
1948        std::fs::remove_file(tmp_snapshot_file_path).unwrap();
1949    }
1950
1951    fn verify_snapshot_recovery(
1952        snapshot_file: impl AsRef<std::path::Path>,
1953        num_threads: usize,
1954        min_record_size: usize,
1955        records_num_per_threads: &Vec<usize>,
1956        check_prefix: bool,
1957    ) {
1958        let bftree = BfTree::new_from_cpr_snapshot(snapshot_file, false, None, None, None)
1959            .expect("fail to recover from snapshot");
1960
1961        let mut rs_captured = vec![0usize; num_threads];
1962        for i in 0..num_threads {
1963            let record_num = records_num_per_threads[i];
1964
1965            let key_len: usize = min_record_size / 2 + std::mem::size_of::<usize>();
1966            let mut key_buffer = vec![0usize; key_len / std::mem::size_of::<usize>()];
1967            let mut res_buffer = vec![0u8; key_len];
1968            let mut not_included = false;
1969            let mut first_gap_record: Option<usize> = None;
1970
1971            for r in 0..record_num {
1972                key_buffer.fill(r);
1973                key_buffer[0] = i;
1974
1975                match bftree.read(
1976                    bytemuck::must_cast_slice::<usize, u8>(&key_buffer),
1977                    &mut res_buffer,
1978                ) {
1979                    LeafReadResult::Found(v) => {
1980                        if check_prefix && not_included {
1981                            // Gather diagnostic info: scan forward to find all gaps
1982                            let mut gaps = Vec::new();
1983                            let mut found_after = Vec::new();
1984                            let gap_start = first_gap_record.unwrap();
1985                            // Collect all gaps in the range [gap_start..r+50]
1986                            let scan_end = std::cmp::min(r + 50, record_num);
1987                            for scan_r in gap_start..scan_end {
1988                                key_buffer.fill(scan_r);
1989                                key_buffer[0] = i;
1990                                match bftree.read(
1991                                    bytemuck::must_cast_slice::<usize, u8>(&key_buffer),
1992                                    &mut res_buffer,
1993                                ) {
1994                                    LeafReadResult::Found(_) => {
1995                                        found_after.push(scan_r);
1996                                    }
1997                                    LeafReadResult::NotFound => {
1998                                        gaps.push(scan_r);
1999                                    }
2000                                    _ => {}
2001                                }
2002                            }
2003                            panic!(
2004                                "PREFIX VIOLATION: thread={}, first_gap_at={}, found_record_after_gap={}, \
2005                                 total_captured_before_gap={}, total_records={}\n\
2006                                 Gaps in [{}, {}): {:?}\n\
2007                                 Found in [{}, {}): {:?}",
2008                                i, gap_start, r, rs_captured[i], record_num,
2009                                gap_start, scan_end, &gaps[..std::cmp::min(gaps.len(), 20)],
2010                                gap_start, scan_end, &found_after[..std::cmp::min(found_after.len(), 20)],
2011                            );
2012                        }
2013                        assert_eq!(v as usize, key_len);
2014                        assert_eq!(
2015                            &res_buffer,
2016                            bytemuck::must_cast_slice::<usize, u8>(&key_buffer)
2017                        );
2018                        rs_captured[i] += 1;
2019                    }
2020                    LeafReadResult::NotFound => {
2021                        if !not_included {
2022                            not_included = true;
2023                            first_gap_record = Some(r);
2024                        }
2025                    }
2026                    _ => {
2027                        panic!("Unexpected read result")
2028                    }
2029                }
2030            }
2031
2032            assert!(rs_captured[i] <= record_num);
2033            println!("Total inserted records for thread {}: {}", i, record_num);
2034            println!(
2035                "Hit ratio for thread {}: {}",
2036                i,
2037                rs_captured[i] as f64 / record_num as f64
2038            );
2039        }
2040    }
2041
2042    /// Inner body for the cache-only CPR snapshot test, parameterized by an
2043    /// iteration id so that concurrent shuttle replicas (and successive shuttle
2044    /// iterations) do not collide on the snapshot file path.
2045    #[cfg(feature = "shuttle")]
2046    fn shuttle_cpr_snapshot_cache_only_inner(iter: usize) {
2047        let min_record_size: usize = 64;
2048        let max_record_size: usize = 2408;
2049        let leaf_page_size: usize = 8192;
2050        let num_threads: usize = 4;
2051        // Bounded number of inserts per writer so shuttle iterations finish quickly.
2052        let inserts_per_thread: usize = 1_000; // 1K inserts per thread
2053
2054        let snapshot_file_path: String = format!(
2055            "target/shuttle_cpr_snapshot_cache_only_{}_{}.bftree",
2056            std::process::id(),
2057            iter,
2058        );
2059        let tmp_snapshot_file_path = std::path::PathBuf::from_str(&snapshot_file_path).unwrap();
2060
2061        let mut config = Config::default();
2062        config.storage_backend(crate::StorageBackend::Memory);
2063        config.file_path(":memory:");
2064        config.cache_only = true;
2065        // Use a buffer sufficient for the test data. 128KB is more than enough
2066        // for 16 small records and avoids allocating 1GB per shuttle iteration.
2067        config.cb_size_byte(1024 * 1024 * 1024);
2068        config.cb_min_record_size = min_record_size;
2069        config.cb_max_record_size = max_record_size;
2070        config.leaf_page_size = leaf_page_size;
2071        config.max_fence_len = max_record_size;
2072        config.use_snapshot(true);
2073
2074        let bftree = Arc::new(BfTree::with_config(config.clone(), None).unwrap());
2075        let mut rs = vec![0usize; num_threads];
2076
2077        for j in 0..2 {
2078            let handles: Vec<_> = (0..num_threads)
2079                .map(|i| {
2080                    let bftree_clone = bftree.clone();
2081                    let start_id = j * inserts_per_thread;
2082                    let end_id = start_id + inserts_per_thread;
2083                    thread::spawn(move || {
2084                        let key_len: usize = min_record_size / 2 + std::mem::size_of::<usize>();
2085                        assert!(key_len * 2 <= max_record_size);
2086                        let mut key_buffer = vec![0usize; key_len / std::mem::size_of::<usize>()];
2087
2088                        for r in start_id..end_id {
2089                            key_buffer.fill(r);
2090                            key_buffer[0] = i;
2091
2092                            match bftree_clone.insert(
2093                                bytemuck::must_cast_slice::<usize, u8>(&key_buffer),
2094                                bytemuck::must_cast_slice::<usize, u8>(&key_buffer),
2095                            ) {
2096                                crate::LeafInsertResult::Success => {}
2097                                _ => {
2098                                    panic!("Insert failed");
2099                                }
2100                            }
2101                        }
2102                        inserts_per_thread
2103                    })
2104                })
2105                .collect();
2106
2107            // Snapshot thread: takes the snapshot concurrently with the writers.
2108            let bftree_for_snap = bftree.clone();
2109            let snap_path = tmp_snapshot_file_path.clone();
2110            let snap_handle = thread::spawn(move || {
2111                bftree_for_snap.cpr_snapshot(&snap_path);
2112            });
2113
2114            for (i, h) in handles.into_iter().enumerate() {
2115                let r = h.join().unwrap();
2116                rs[i] += r;
2117            }
2118
2119            // Verify the snapshot taken is valid
2120            snap_handle.join().unwrap();
2121            let snap_path = tmp_snapshot_file_path.clone();
2122            verify_snapshot_recovery(&snap_path, num_threads, min_record_size, &rs, false);
2123        }
2124
2125        let _ = std::fs::remove_file(tmp_snapshot_file_path);
2126    }
2127
2128    /// Testing
2129    #[cfg(feature = "shuttle")]
2130    #[test]
2131    fn shuttle_cpr_snapshot_cache_only() {
2132        use std::sync::atomic::AtomicUsize;
2133
2134        // Unique iteration id so portfolio replicas / successive iterations do
2135        // not collide on the snapshot file path.
2136        static ITER: AtomicUsize = AtomicUsize::new(0);
2137
2138        let mut shuttle_config = shuttle::Config::default();
2139        //shuttle_config.max_steps = shuttle::MaxSteps::FailAfter(100_000);
2140        shuttle_config.max_steps = shuttle::MaxSteps::None;
2141        shuttle_config.stack_size = 1024 * 1024 * 1024; // 1GB — default 32KB overflows with deep tree ops
2142        shuttle_config.failure_persistence =
2143            shuttle::FailurePersistence::File(Some(PathBuf::from_str("target").unwrap()));
2144
2145        let mut runner = shuttle::PortfolioRunner::new(true, shuttle_config);
2146        let available_cores = std::thread::available_parallelism().unwrap().get().min(4);
2147        for _ in 0..available_cores {
2148            runner.add(shuttle::scheduler::PctScheduler::new(10, 1000));
2149        }
2150
2151        runner.run(|| {
2152            let iter = ITER.fetch_add(1, Ordering::Relaxed);
2153            shuttle_cpr_snapshot_cache_only_inner(iter);
2154            eprintln!("Completed shuttle iteration {}", iter);
2155        });
2156    }
2157
2158    /// Inner body for the disk CPR snapshot shuttle test, parameterized by an
2159    /// iteration id so that concurrent shuttle replicas (and successive shuttle
2160    /// iterations) do not collide on file paths.
2161    #[cfg(feature = "shuttle")]
2162    fn shuttle_cpr_snapshot_disk_inner(iter: usize) {
2163        let min_record_size: usize = 64;
2164        let max_record_size: usize = 2408;
2165        let leaf_page_size: usize = 8192;
2166        let num_threads: usize = 4;
2167        // 500 inserts/thread × 4 threads = 2000 records/round.
2168        // With 128KB buffer (~1600 record capacity), this triggers eviction
2169        // while staying within shuttle coroutine stack limits.
2170        let inserts_per_thread: usize = 500;
2171
2172        let file_path: String = format!(
2173            "target/shuttle_cpr_snapshot_disk_{}_{}.bftree",
2174            std::process::id(),
2175            iter,
2176        );
2177        let snapshot_file_path: String = format!(
2178            "target/shuttle_cpr_snapshot_disk_{}_{}_snap.bftree",
2179            std::process::id(),
2180            iter,
2181        );
2182        let tmp_file_path = std::path::PathBuf::from_str(&file_path).unwrap();
2183        let tmp_snapshot_file_path = std::path::PathBuf::from_str(&snapshot_file_path).unwrap();
2184
2185        let mut config = Config::new(&tmp_file_path, 128 * 1024); // 128KB buffer, triggers eviction
2186        config.storage_backend(crate::StorageBackend::Std);
2187        config.cb_min_record_size = min_record_size + 2 * std::mem::size_of::<usize>();
2188        config.cb_max_record_size = max_record_size;
2189        config.leaf_page_size = leaf_page_size;
2190        config.max_fence_len = min_record_size + 2 * std::mem::size_of::<usize>();
2191        config.use_snapshot(true);
2192
2193        let bftree = Arc::new(BfTree::with_config(config.clone(), None).unwrap());
2194        let mut rs = vec![0usize; num_threads];
2195        for j in 0..3 {
2196            let handles: Vec<_> = (0..num_threads)
2197                .map(|i| {
2198                    let bftree_clone = bftree.clone();
2199                    // Always use key range 0..inserts_per_thread so shuttle_replay
2200                    // can reproduce any failing iteration with iter=0.
2201                    let start_id = j * inserts_per_thread;
2202                    let end_id = start_id + inserts_per_thread;
2203                    thread::spawn(move || {
2204                        let key_len: usize = min_record_size / 2 + std::mem::size_of::<usize>();
2205                        assert!(key_len * 2 <= max_record_size);
2206                        let mut key_buffer = vec![0usize; key_len / std::mem::size_of::<usize>()];
2207
2208                        for r in start_id..end_id {
2209                            key_buffer.fill(r);
2210                            key_buffer[0] = i;
2211
2212                            match bftree_clone.insert(
2213                                bytemuck::must_cast_slice::<usize, u8>(&key_buffer),
2214                                bytemuck::must_cast_slice::<usize, u8>(&key_buffer),
2215                            ) {
2216                                crate::LeafInsertResult::Success => {}
2217                                _ => {
2218                                    panic!("Insert failed");
2219                                }
2220                            }
2221                        }
2222                        inserts_per_thread
2223                    })
2224                })
2225                .collect();
2226
2227            // Snapshot thread: takes the snapshot concurrently with the writers.
2228            let bftree_for_snap = bftree.clone();
2229            let snap_path = tmp_snapshot_file_path.clone();
2230            let snap_handle = thread::spawn(move || {
2231                bftree_for_snap.cpr_snapshot(&snap_path);
2232            });
2233
2234            for (i, h) in handles.into_iter().enumerate() {
2235                let r = h.join().unwrap();
2236                rs[i] += r;
2237            }
2238
2239            snap_handle.join().unwrap();
2240
2241            // Recover from the snapshot and verify invariants
2242            verify_snapshot_recovery(
2243                &tmp_snapshot_file_path,
2244                num_threads,
2245                min_record_size,
2246                &rs,
2247                true,
2248            );
2249        }
2250        let _ = std::fs::remove_file(tmp_file_path);
2251        let _ = std::fs::remove_file(tmp_snapshot_file_path);
2252    }
2253
2254    #[cfg(feature = "shuttle")]
2255    #[test]
2256    fn shuttle_cpr_snapshot_disk() {
2257        use std::sync::atomic::AtomicUsize;
2258
2259        // Unique iteration id so portfolio replicas / successive iterations do
2260        // not collide on file paths.
2261        static ITER: AtomicUsize = AtomicUsize::new(0);
2262
2263        let mut shuttle_config = shuttle::Config::default();
2264        shuttle_config.max_steps = shuttle::MaxSteps::None;
2265        // Default shuttle stack is 32KB which is too small for deep B-tree
2266        // operations with eviction + file I/O. Increase to avoid stack overflow
2267        // that manifests as STATUS_HEAP_CORRUPTION on Windows.
2268        shuttle_config.stack_size = 4 * 1024 * 1024; // 4MB
2269        shuttle_config.failure_persistence =
2270            shuttle::FailurePersistence::File(Some(PathBuf::from_str("target").unwrap()));
2271
2272        let mut runner = shuttle::PortfolioRunner::new(true, shuttle_config);
2273        let available_cores = std::thread::available_parallelism().unwrap().get().min(4);
2274        for _ in 0..available_cores {
2275            runner.add(shuttle::scheduler::PctScheduler::new(10, 100));
2276        }
2277
2278        runner.run(|| {
2279            let iter = ITER.fetch_add(1, Ordering::Relaxed);
2280            shuttle_cpr_snapshot_disk_inner(iter);
2281        });
2282    }
2283
2284    #[cfg(feature = "shuttle")]
2285    #[test]
2286    fn shuttle_replay() {
2287        let schedule_path = "target/schedule000.txt";
2288        if !std::path::Path::new(schedule_path).exists() {
2289            eprintln!("No schedule file at {schedule_path}; run shuttle_cpr_snapshot_disk to generate one on failure.");
2290            return;
2291        }
2292
2293        // install global collector configured based on RUST_LOG env var.
2294        tracing_subscriber::fmt()
2295            .with_ansi(true)
2296            .with_thread_names(false)
2297            .with_target(false)
2298            .init();
2299
2300        shuttle::replay_from_file(|| shuttle_cpr_snapshot_disk_inner(0), schedule_path);
2301    }
2302}