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