Skip to main content

reddb_server/storage/unified/store/
commit.rs

1use super::*;
2use crate::api::DurabilityMode;
3use crate::storage::wal::{WalReader, WalRecord, WalWriter};
4use reddb_engine::storage::resident_file::EMBEDDED_WAL_HEADROOM_LIMIT;
5use std::cell::RefCell;
6use std::io;
7use std::path::{Path, PathBuf};
8use std::sync::atomic::{AtomicU64, AtomicU8, Ordering};
9use std::sync::{Arc, Condvar, Mutex};
10
11#[path = "checkpoint_undo.rs"]
12mod checkpoint_undo;
13
14struct NativeReplayTarget<'a> {
15    store: &'a UnifiedStore,
16    update_btrees: bool,
17}
18
19impl reddb_engine::storage::store_wal::StoreReplayTarget for NativeReplayTarget<'_> {
20    fn set_aux_metadata(&self, bytes: &[u8]) -> Result<(), StoreError> {
21        let bytes = reddb_engine::catalog::codec::canonicalize_collection_contract_blob(bytes)
22            .map_err(|err| StoreError::Serialization(err.to_string()))?;
23        self.store.resident.set_aux_metadata(bytes);
24        Ok(())
25    }
26    fn has_collection(&self, name: &str) -> bool {
27        self.store.get_collection(name).is_some()
28    }
29    fn create_collection(&self, name: &str) -> Result<(), StoreError> {
30        self.store.create_collection_in_memory(name)
31    }
32    fn drop_collection(&self, name: &str) -> Result<(), StoreError> {
33        self.store.drop_collection_in_memory(name)
34    }
35    fn upsert(&self, collection: &str, record: &[u8]) -> Result<(), StoreError> {
36        self.store
37            .apply_replayed_upsert(collection, record, self.update_btrees)
38    }
39    fn delete(&self, collection: &str, id: EntityId) -> Result<(), StoreError> {
40        self.store
41            .apply_replayed_delete(collection, id, self.update_btrees)
42    }
43    fn refresh(&self, collection: &str, records: &[Vec<u8>]) -> Result<(), StoreError> {
44        self.store
45            .apply_replayed_refresh_collection(collection, records, self.update_btrees)
46    }
47}
48
49/// Adaptive group-commit window applied when the configured
50/// `GroupCommitOptions::window_ms` is 0 (the historical default).
51///
52/// Sub-millisecond so individual single-writer commits don't see a
53/// visible latency penalty (a 200 µs floor sits below typical NVMe
54/// fsync latency of 50–150 µs by ~one fsync), while still giving a
55/// pipelined writer a chance to drop a second statement into the
56/// same drain cycle. A lone synchronous `insert_one` still pays one
57/// fsync per row in the worst case, but two back-to-back inserts on
58/// the same connection now coalesce into one drain. See P1 in
59/// `docs/perf/insert_sequential-2026-05-05.md`.
60///
61/// Override via `REDDB_GROUP_COMMIT_WINDOW_US` (microseconds). Set
62/// to `0` to disable the floor entirely (legacy behaviour). Set
63/// `REDDB_GROUP_COMMIT_WINDOW_MS` to any non-zero value to bypass
64/// this floor — explicit ms config wins.
65const DEFAULT_ADAPTIVE_WINDOW_US: u64 = 200;
66
67/// Shorthand — `Arc<parking_lot::Mutex<WalWriter>>` avoids poisoning
68/// (one writer panicking mid-append used to taint every subsequent
69/// lock acquisition) and shaves a few syscalls off the fast path.
70/// The group-commit coordinator + writer threads all acquire this
71/// mutex in the hot insert path, so the unpoison/fast-park win
72/// compounds under 16-way concurrency.
73type WalMutex = parking_lot::Mutex<WalWriter>;
74
75/// Shorthand for the group-commit coordinator's state pair. Same
76/// non-poisoning + lighter-park motivation as `WalMutex`; writer
77/// threads `wait` on this condvar until the coordinator publishes a
78/// new `durable_lsn`, so the park cost shows up on every
79/// WalDurableGrouped transaction.
80type CommitStateMutex = parking_lot::Mutex<CommitState>;
81type CommitStateCondvar = parking_lot::Condvar;
82use std::time::{Duration, Instant};
83
84static NEXT_STORE_TX_ID: AtomicU64 = AtomicU64::new(1);
85
86pub(crate) use reddb_engine::storage::store_wal::StoreWalAction;
87
88#[derive(Debug, Default)]
89pub(crate) struct DeferredStoreWalActions {
90    pub(super) actions: Vec<StoreWalAction>,
91}
92
93pub(super) struct StoreWalCaptureGuard<'a> {
94    active: bool,
95    session: Option<&'a crate::storage::embedded::EmbeddedCommitSession>,
96}
97
98impl<'a> StoreWalCaptureGuard<'a> {
99    pub(super) fn begin(
100        session: Option<&'a crate::storage::embedded::EmbeddedCommitSession>,
101    ) -> Self {
102        assert!(
103            !deferred_store_wal_capture_active(),
104            "store WAL captures cannot nest"
105        );
106        begin_deferred_store_wal_capture();
107        Self {
108            active: true,
109            session,
110        }
111    }
112
113    pub(super) fn finish(mut self) -> DeferredStoreWalActions {
114        let actions = take_deferred_store_wal_capture();
115        self.active = false;
116        actions
117    }
118}
119
120impl Drop for StoreWalCaptureGuard<'_> {
121    fn drop(&mut self) {
122        if self.active {
123            if let Some(session) = self.session {
124                session.fence_interrupted_write();
125            }
126            let _ = take_deferred_store_wal_capture();
127        }
128    }
129}
130
131impl DeferredStoreWalActions {
132    pub(crate) fn is_empty(&self) -> bool {
133        self.actions.is_empty()
134    }
135
136    pub(crate) fn extend(&mut self, other: Self) {
137        self.actions.extend(other.actions);
138    }
139}
140
141thread_local! {
142    static DEFERRED_STORE_WAL_ACTIONS: RefCell<Option<Vec<StoreWalAction>>> =
143        const { RefCell::new(None) };
144}
145
146fn begin_deferred_store_wal_capture() {
147    DEFERRED_STORE_WAL_ACTIONS.with(|cell| {
148        let mut guard = cell.borrow_mut();
149        debug_assert!(guard.is_none());
150        *guard = Some(Vec::new());
151    });
152}
153
154fn capture_deferred_store_wal_actions(actions: Vec<StoreWalAction>) -> bool {
155    DEFERRED_STORE_WAL_ACTIONS.with(|cell| {
156        let mut guard = cell.borrow_mut();
157        if let Some(pending) = guard.as_mut() {
158            pending.extend(actions);
159            true
160        } else {
161            false
162        }
163    })
164}
165
166fn deferred_store_wal_capture_active() -> bool {
167    DEFERRED_STORE_WAL_ACTIONS.with(|cell| cell.borrow().is_some())
168}
169
170fn take_deferred_store_wal_capture() -> DeferredStoreWalActions {
171    DEFERRED_STORE_WAL_ACTIONS.with(|cell| DeferredStoreWalActions {
172        actions: cell.borrow_mut().take().unwrap_or_default(),
173    })
174}
175
176#[derive(Debug)]
177struct CommitState {
178    durable_lsn: u64,
179    pending_target_lsn: u64,
180    pending_statements: usize,
181    pending_wal_bytes: u64,
182    first_pending_at: Option<Instant>,
183    shutdown: bool,
184    last_error: Option<String>,
185}
186
187impl CommitState {
188    fn new(initial_durable_lsn: u64) -> Self {
189        Self {
190            durable_lsn: initial_durable_lsn,
191            pending_target_lsn: initial_durable_lsn,
192            pending_statements: 0,
193            pending_wal_bytes: 0,
194            first_pending_at: None,
195            shutdown: false,
196            last_error: None,
197        }
198    }
199}
200
201/// Lock-free append queue sitting in front of `WalWriter`.
202///
203/// Writers atomically reserve a byte range via `next_lsn.fetch_add`
204/// and push their encoded bytes into a parking_lot-guarded vector.
205/// The group-commit coordinator is the sole drainer: it sorts the
206/// pending entries by LSN (so the file bytes land at the offsets
207/// each writer reserved) and hands them to `WalWriter::append_bytes`.
208///
209/// This replaces the old `wal.lock() ... append ... append ... drop`
210/// hot path where 16 concurrent writers serialised on the WAL
211/// mutex for ~13µs each. Hold time on the queue's parking_lot
212/// mutex is ~200ns (just a Vec::push) — 65× shorter, so the mutex
213/// convoy on concurrent inserts disappears.
214pub(crate) struct WalAppendQueue {
215    /// Keep reserved offsets valid until their callers observe durability.
216    /// Reset also excludes an old flusher publishing state into the new WAL.
217    epoch: parking_lot::RwLock<()>,
218    /// Tuple of (monotonically-increasing LSN, pending-bytes vec)
219    /// protected by one mutex. Keeping the LSN reservation AND the
220    /// push under the same lock guarantees that every queue entry
221    /// is visible the moment its LSN is assigned — no gap between
222    /// `fetch_add` and `push` for the leader to spin on.
223    ///
224    /// Earlier versions reserved LSN with an `AtomicU64::fetch_add`
225    /// outside the lock; the leader drain observed reordered
226    /// `(lsn, bytes)` tuples whose pushes happened in a different
227    /// order than their fetch_add, creating "holes" in the LSN
228    /// sequence that the drain loop interpreted as "wait for the
229    /// missing enqueuer" — under tokio scheduling pressure the
230    /// missing enqueuer was preempted indefinitely and the
231    /// drain loop busy-waited forever (WAL stayed at 8 bytes).
232    pending: parking_lot::Mutex<WalQueueState>,
233    /// Bytes of encoded records sitting in `pending.entries` (ADR 0073 §2,
234    /// the `wal_buffers` pool). A relaxed atomic rather than a field inside
235    /// `WalQueueState` so a reader sampling the accounting never contends
236    /// with an enqueuer for the queue mutex.
237    queued_bytes: AtomicU64,
238}
239
240struct WalQueueState {
241    next_lsn: u64,
242    entries: Vec<(u64, Vec<u8>)>,
243}
244
245impl WalAppendQueue {
246    fn new(initial_lsn: u64) -> Self {
247        Self {
248            epoch: parking_lot::RwLock::new(()),
249            pending: parking_lot::Mutex::new(WalQueueState {
250                next_lsn: initial_lsn,
251                entries: Vec::with_capacity(64),
252            }),
253            queued_bytes: AtomicU64::new(0),
254        }
255    }
256
257    /// Reserve an LSN range of `bytes.len()` bytes and push onto the
258    /// queue. Returns the commit LSN (end of reserved range), which
259    /// the caller passes to `wait_until_durable`. LSN assignment and
260    /// push happen under the same mutex — no gap for the drain loop
261    /// to busy-spin on.
262    fn enqueue(&self, bytes: Vec<u8>) -> u64 {
263        let len = bytes.len() as u64;
264        let mut state = self.pending.lock();
265        let start_lsn = state.next_lsn;
266        state.next_lsn = start_lsn + len;
267        state.entries.push((start_lsn, bytes));
268        self.queued_bytes.fetch_add(len, Ordering::Relaxed);
269        start_lsn + len
270    }
271
272    /// Drain all queued entries in LSN order. Caller holds the WAL
273    /// file mutex while writing the drained bytes so the on-disk
274    /// layout matches the reserved LSN offsets.
275    fn drain_sorted(&self) -> Vec<(u64, Vec<u8>)> {
276        let mut state = self.pending.lock();
277        let mut v = std::mem::take(&mut state.entries);
278        let drained: u64 = v.iter().map(|(_, bytes)| bytes.len() as u64).sum();
279        drop(state);
280        // Saturating: the drain is the only consumer, so `queued_bytes` can
281        // never sit below what this call took, but an accounting counter that
282        // wraps would poison `red.stats` far past the bug that caused it.
283        let _ = self
284            .queued_bytes
285            .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |queued| {
286                Some(queued.saturating_sub(drained))
287            });
288        v.sort_by_key(|(lsn, _)| *lsn);
289        v
290    }
291
292    /// Whether any entry is queued. Leader uses this to decide
293    /// whether to spin once more or go back to the condvar.
294    fn has_pending(&self) -> bool {
295        !self.pending.lock().entries.is_empty()
296    }
297
298    /// Encoded record bytes currently buffered in memory.
299    fn queued_bytes(&self) -> u64 {
300        self.queued_bytes.load(Ordering::Relaxed)
301    }
302
303    /// Reset the LSN cursor and discard any queued entries. Used
304    /// after `wal.truncate()` — the wal-side byte counter goes back
305    /// to the header size, so the queue (which tracks LSNs in the
306    /// same byte space) must follow or every subsequent enqueue
307    /// returns a target the drain loop can never reach.
308    fn reset(&self, next_lsn: u64) {
309        let mut state = self.pending.lock();
310        state.next_lsn = next_lsn;
311        state.entries.clear();
312        drop(state);
313        self.queued_bytes.store(0, Ordering::Relaxed);
314    }
315}
316
317pub(crate) struct StoreCommitCoordinator {
318    mode: DurabilityMode,
319    config: crate::api::GroupCommitOptions,
320    wal_path: PathBuf,
321    wal: Arc<WalMutex>,
322    /// Lock-free front door for writers. Populated alongside
323    /// `WalDurableGrouped` / `Async` modes so concurrent inserts
324    /// never contend on `wal` for the append step. Strict mode
325    /// bypasses the queue and calls `WalWriter::append` directly
326    /// to preserve the one-fsync-per-commit semantic.
327    queue: Arc<WalAppendQueue>,
328    state: Arc<(CommitStateMutex, CommitStateCondvar)>,
329    /// Number of `wal.sync()` calls issued by the group-commit
330    /// drain loop. Used by tests to observe coalescing — a burst
331    /// of N concurrent commits should bump this by far less than N
332    /// when the adaptive window is doing its job. Strict-mode
333    /// commits go through `force_sync` which also bumps this.
334    fsync_count: Arc<AtomicU64>,
335    wal_generation: AtomicU64,
336    /// Includes recovered epochs that were never published in the catalog.
337    ownership_last_epoch: AtomicU64,
338    // 0: existing WAL not scanned; 1: no ownership records; 2: protected.
339    ownership_wal_state: AtomicU8,
340    // 0: existing WAL not scanned; 1: no runtime records; 2: retained.
341    runtime_wal_state: AtomicU8,
342}
343
344/// Connects a fenced pager ownership epoch to the store's durable WAL.
345#[allow(dead_code)] // Installed once recovery and format fencing are active.
346struct StoreOwnershipRecorder {
347    coordinator: Arc<StoreCommitCoordinator>,
348    epoch: u64,
349    images: parking_lot::Mutex<CheckpointPageImages>,
350}
351
352#[derive(Default)]
353struct CheckpointPageImages {
354    reserved: roaring::RoaringBitmap,
355    captured: roaring::RoaringBitmap,
356}
357
358impl StoreOwnershipRecorder {
359    #[allow(dead_code)]
360    fn begin(coordinator: Arc<StoreCommitCoordinator>, epoch: u64) -> io::Result<Self> {
361        coordinator.append_durable_page_ownership(
362            reddb_file::page_ownership_journal::PageOwnershipRecord::Begin { epoch },
363        )?;
364        Ok(Self {
365            coordinator,
366            epoch,
367            images: parking_lot::Mutex::new(CheckpointPageImages::default()),
368        })
369    }
370
371    fn append_pages(
372        &self,
373        pages: &[u32],
374        make_record: impl Fn(u64, u32) -> reddb_file::page_ownership_journal::PageOwnershipRecord,
375    ) -> io::Result<()> {
376        for chunk in pages.chunks(256) {
377            let records: Vec<_> = chunk
378                .iter()
379                .map(|&id| make_record(self.epoch, id))
380                .collect();
381            self.coordinator
382                .append_durable_page_ownership_batch(&records)?;
383        }
384        Ok(())
385    }
386}
387
388impl crate::storage::engine::pager::PageOwnershipRecorder for StoreOwnershipRecorder {
389    fn needs_page_image(&self, page_id: u32) -> io::Result<bool> {
390        // The manifest is the commit boundary, never an undo target. Its
391        // private overflow replacement is already covered by reservations.
392        if page_id < reddb_engine::storage::pager_io::FIRST_ALLOCATABLE_PAGE {
393            return Ok(false);
394        }
395        let images = self.images.lock();
396        Ok(!images.reserved.contains(page_id) && !images.captured.contains(page_id))
397    }
398
399    fn capture_page_image(&self, page: &crate::storage::engine::Page) -> io::Result<()> {
400        let mut images = self.images.lock();
401        let page_id = page.page_id();
402        if images.reserved.contains(page_id) || images.captured.contains(page_id) {
403            return Ok(());
404        }
405        let mut bytes = Vec::new();
406        reddb_file::encode_main_wal_record_frame_into(
407            reddb_file::MainWalRecordFrameRef::FullPageImage {
408                tx_id: 0,
409                page_id,
410                ckpt_epoch: self.epoch,
411                data: page.as_bytes(),
412            },
413            crate::storage::wal::record::WAL_DEFAULT_TERM,
414            &mut bytes,
415        )?;
416        self.coordinator.append_durable_ownership_bytes(bytes)?;
417        images.captured.insert(page_id);
418        Ok(())
419    }
420
421    fn reserve_pages(&self, start: u32, count: u32) -> io::Result<()> {
422        use reddb_file::page_ownership_journal::PageOwnershipRecord;
423        let end = start.checked_add(count).ok_or_else(|| {
424            io::Error::new(io::ErrorKind::InvalidInput, "ownership range overflow")
425        })?;
426        let mut next = start;
427        while next < end {
428            let chunk_end = next.saturating_add(256).min(end);
429            let records: Vec<_> = (next..chunk_end)
430                .map(|page_id| PageOwnershipRecord::Reserve {
431                    epoch: self.epoch,
432                    page_id,
433                })
434                .collect();
435            self.coordinator
436                .append_durable_page_ownership_batch(&records)?;
437            self.images.lock().reserved.insert_range(next..chunk_end);
438            next = chunk_end;
439        }
440        Ok(())
441    }
442
443    fn detach_free_pages(&self, pages: &[u32]) -> io::Result<()> {
444        self.append_pages(pages, |epoch, page_id| {
445            reddb_file::page_ownership_journal::PageOwnershipRecord::DetachFree { epoch, page_id }
446        })
447    }
448
449    fn retire_pages(&self, pages: &[u32]) -> io::Result<()> {
450        self.append_pages(pages, |epoch, page_id| {
451            reddb_file::page_ownership_journal::PageOwnershipRecord::Retire { epoch, page_id }
452        })
453    }
454}
455
456impl StoreCommitCoordinator {
457    pub(crate) fn open(
458        wal_path: impl Into<PathBuf>,
459        mode: DurabilityMode,
460        config: crate::api::GroupCommitOptions,
461    ) -> io::Result<Self> {
462        let wal_path = wal_path.into();
463        let wal = WalWriter::open(&wal_path)?;
464        let initial_durable_lsn = wal.durable_lsn();
465        let initial_current_lsn = wal.current_lsn();
466        let wal = Arc::new(WalMutex::new(wal));
467        let queue = Arc::new(WalAppendQueue::new(initial_current_lsn));
468        let state = Arc::new((
469            CommitStateMutex::new(CommitState::new(initial_durable_lsn)),
470            CommitStateCondvar::new(),
471        ));
472        let fsync_count = Arc::new(AtomicU64::new(0));
473
474        if matches!(
475            mode,
476            DurabilityMode::WalDurableGrouped | DurabilityMode::Async
477        ) {
478            let wal_bg = Arc::clone(&wal);
479            let queue_bg = Arc::clone(&queue);
480            let state_bg = Arc::clone(&state);
481            let fsync_bg = Arc::clone(&fsync_count);
482            // P1: adaptive group-commit window. Historical default is
483            // `window_ms = 0` ("no wait"), which under single-writer
484            // OLTP means one fsync per autocommit row — the throughput
485            // floor. When window_ms is 0 we fall back to a small
486            // microsecond floor (`DEFAULT_ADAPTIVE_WINDOW_US`, override
487            // via `REDDB_GROUP_COMMIT_WINDOW_US`) so a pipelined writer
488            // can drop a second statement into the same drain cycle.
489            // Explicit non-zero `window_ms` config takes precedence.
490            //
491            // The loop already short-circuits on
492            // `pending_statements >= max_statements` /
493            // `pending_wal_bytes >= max_wal_bytes`, so the window only
494            // delays the leader when the queue is otherwise empty —
495            // exactly the case we want to coalesce.
496            let window = Self::resolve_window(&config);
497            let max_statements = config.max_statements.max(1);
498            let max_wal_bytes = config.max_wal_bytes.max(1);
499            std::thread::spawn(move || {
500                Self::run_group_commit_loop(
501                    wal_bg,
502                    queue_bg,
503                    state_bg,
504                    fsync_bg,
505                    window,
506                    max_statements,
507                    max_wal_bytes,
508                );
509            });
510        }
511
512        Ok(Self {
513            mode,
514            config,
515            wal_path,
516            wal,
517            queue,
518            state,
519            fsync_count,
520            wal_generation: AtomicU64::new(0),
521            ownership_last_epoch: AtomicU64::new(0),
522            ownership_wal_state: AtomicU8::new(0),
523            runtime_wal_state: AtomicU8::new(0),
524        })
525    }
526
527    /// Resolve the effective group-commit window from the configured
528    /// options + the `REDDB_GROUP_COMMIT_WINDOW_US` env override.
529    ///
530    /// Precedence (highest first):
531    ///   1. `REDDB_GROUP_COMMIT_WINDOW_US=N` — N µs (0 disables).
532    ///   2. `config.window_ms != 0` — explicit ms config wins.
533    ///   3. `DEFAULT_ADAPTIVE_WINDOW_US` — adaptive floor for the
534    ///      historical zero-default. Set the env var to 0 to opt out.
535    fn resolve_window(config: &crate::api::GroupCommitOptions) -> Duration {
536        if let Ok(raw) = std::env::var("REDDB_GROUP_COMMIT_WINDOW_US") {
537            if let Ok(parsed) = raw.parse::<u64>() {
538                return Duration::from_micros(parsed);
539            }
540        }
541        if config.window_ms != 0 {
542            return Duration::from_millis(config.window_ms);
543        }
544        Duration::from_micros(DEFAULT_ADAPTIVE_WINDOW_US)
545    }
546
547    /// Total `wal.sync()` calls issued since this coordinator opened.
548    /// Public for tests that want to observe fsync coalescing under
549    /// concurrent autocommits.
550    #[cfg(test)]
551    pub(crate) fn fsync_count(&self) -> u64 {
552        self.fsync_count.load(Ordering::Relaxed)
553    }
554
555    /// WAL buffer memory held right now: encoded records waiting in the
556    /// group-commit queue plus the writer's fixed append buffer, which exists
557    /// for the coordinator's whole life (ADR 0073 §2, the `wal_buffers` pool).
558    pub(crate) fn buffered_bytes(&self) -> u64 {
559        self.queue.queued_bytes() + crate::storage::wal::WAL_BUFFER_BYTES as u64
560    }
561
562    pub(crate) fn append_actions(&self, actions: &[StoreWalAction]) -> io::Result<()> {
563        self.append_actions_impl(actions, false).map(|_| ())
564    }
565
566    pub(super) fn append_checkpoint_actions(
567        &self,
568        actions: &[StoreWalAction],
569    ) -> io::Result<super::checkpoint_wal::CheckpointWalBatch> {
570        self.append_actions_impl(actions, true)
571    }
572
573    fn append_actions_impl(
574        &self,
575        actions: &[StoreWalAction],
576        retain: bool,
577    ) -> io::Result<super::checkpoint_wal::CheckpointWalBatch> {
578        use super::checkpoint_wal::CheckpointWalBatch;
579        if actions.is_empty() {
580            return Ok(CheckpointWalBatch::default());
581        }
582        let _epoch = self.queue.epoch.read();
583        let tx_id = NEXT_STORE_TX_ID.fetch_add(1, Ordering::SeqCst);
584        let encoded: Vec<Vec<u8>> = actions.iter().map(StoreWalAction::encode).collect();
585        let wal_bytes = encoded.iter().fold(0u64, |total, action| {
586            total.saturating_add(action.len() as u64)
587        });
588        let (blob, mut references) = if retain {
589            CheckpointWalBatch::encode(
590                tx_id,
591                &encoded,
592                self.wal_generation.load(Ordering::Acquire),
593            )?
594        } else {
595            (
596                WalRecord::TxCommitBatch {
597                    tx_id,
598                    actions: encoded,
599                }
600                .encode(),
601                CheckpointWalBatch::default(),
602            )
603        };
604        let start_lsn = if matches!(self.mode, DurabilityMode::Strict) {
605            let start = self.wal.lock().append_bytes(&blob)?;
606            self.force_sync()?;
607            start
608        } else {
609            let length = blob.len() as u64;
610            let end = self.queue.enqueue(blob);
611            self.wait_until_durable(end, wal_bytes)?;
612            end.checked_sub(length).ok_or_else(|| {
613                io::Error::new(io::ErrorKind::InvalidData, "invalid WAL batch offset")
614            })?
615        };
616        references.relocate(start_lsn)?;
617        Ok(references)
618    }
619
620    /// Compare exact durable bytes in bounded chunks. A proof from an older
621    /// truncated WAL generation cannot prove a row in the current file.
622    pub(super) fn checkpoint_row_matches(
623        &self,
624        reference: super::checkpoint_wal::WalRowReference,
625        expected: &[u8],
626    ) -> io::Result<bool> {
627        if expected.len() != reference.length as usize {
628            return Ok(false);
629        }
630        let mut position = 0;
631        let mut matches = true;
632        let present = self.visit_checkpoint_row(reference, |chunk| {
633            matches &= chunk == &expected[position..position + chunk.len()];
634            position += chunk.len();
635        })?;
636        Ok(present && matches)
637    }
638
639    /// Materialize only one already-validated durable row for a projection batch.
640    pub(super) fn checkpoint_row_bytes(
641        &self,
642        reference: super::checkpoint_wal::WalRowReference,
643    ) -> io::Result<Vec<u8>> {
644        let mut bytes = Vec::new();
645        let present = self.visit_checkpoint_row(reference, |chunk| {
646            if bytes.is_empty() {
647                bytes.reserve_exact(reference.length as usize);
648            }
649            bytes.extend_from_slice(chunk);
650        })?;
651        if !present {
652            return Err(io::Error::new(
653                io::ErrorKind::InvalidData,
654                "checkpoint WAL reference changed during projection",
655            ));
656        }
657        Ok(bytes)
658    }
659
660    fn visit_checkpoint_row(
661        &self,
662        reference: super::checkpoint_wal::WalRowReference,
663        mut visit: impl FnMut(&[u8]),
664    ) -> io::Result<bool> {
665        use std::io::{Read, Seek, SeekFrom};
666        let _epoch = self.queue.epoch.read_recursive();
667        if reference.generation != self.wal_generation.load(Ordering::Acquire) {
668            return Ok(false);
669        }
670        let end = reference
671            .offset
672            .checked_add(u64::from(reference.length))
673            .ok_or_else(|| {
674                io::Error::new(io::ErrorKind::InvalidData, "WAL proof offset overflow")
675            })?;
676        let (state_lock, _) = &*self.state;
677        if end > state_lock.lock().durable_lsn {
678            return Ok(false);
679        }
680        let mut file = std::fs::File::open(&self.wal_path)?;
681        file.seek(SeekFrom::Start(reference.offset))?;
682        let mut buffer = [0u8; 16 * 1024];
683        let mut checksum = crc32fast::Hasher::new();
684        let mut remaining = reference.length as usize;
685        while remaining != 0 {
686            let length = remaining.min(buffer.len());
687            let chunk = &mut buffer[..length];
688            file.read_exact(chunk)?;
689            checksum.update(chunk);
690            visit(chunk);
691            remaining -= length;
692        }
693        if checksum.finalize() != reference.checksum {
694            return Err(io::Error::new(
695                io::ErrorKind::InvalidData,
696                "checkpoint WAL proof checksum mismatch",
697            ));
698        }
699        Ok(true)
700    }
701
702    /// Encode + enqueue a single, non-batched [`WalRecord`] (issue
703    /// #693). Honours the same Strict / Grouped / Async branching as
704    /// [`Self::append_actions`] so durability semantics are uniform
705    /// across record kinds.
706    pub(crate) fn append_single_record(&self, record: WalRecord) -> io::Result<()> {
707        if matches!(record, WalRecord::PageOwnership { .. }) {
708            return Err(io::Error::new(
709                io::ErrorKind::InvalidInput,
710                "page ownership requires a durable append",
711            ));
712        }
713        let _epoch = self.queue.epoch.read();
714        if matches!(
715            record,
716            WalRecord::ProbabilisticDelta { .. } | WalRecord::VectorInsert { .. }
717        ) {
718            self.runtime_wal_state.store(2, Ordering::Release);
719        }
720        let blob = record.encode();
721        let wal_bytes = blob.len() as u64;
722        if matches!(self.mode, DurabilityMode::Strict) {
723            {
724                let mut wal = self.wal.lock();
725                wal.append(&record)?;
726            }
727            self.force_sync()?;
728            return Ok(());
729        }
730        let commit_lsn = self.queue.enqueue(blob);
731        self.wait_until_durable(commit_lsn, wal_bytes)?;
732        Ok(())
733    }
734
735    /// Ownership intents must reach stable storage before the pager consumes
736    /// or changes a page, even when user transactions use async durability.
737    #[allow(dead_code)] // Activated with the pager recovery and format fence.
738    pub(super) fn append_durable_page_ownership(
739        &self,
740        record: reddb_file::page_ownership_journal::PageOwnershipRecord,
741    ) -> io::Result<()> {
742        self.append_durable_page_ownership_batch(&[record])
743    }
744
745    /// Append a bounded set of ownership intents behind one durability fence.
746    #[allow(dead_code)] // Activated with the pager recovery and format fence.
747    pub(super) fn append_durable_page_ownership_batch(
748        &self,
749        records: &[reddb_file::page_ownership_journal::PageOwnershipRecord],
750    ) -> io::Result<()> {
751        if records.is_empty() {
752            return Ok(());
753        }
754        let mut bytes = Vec::new();
755        for &record in records {
756            // Validate before WalRecord::encode, which expects a valid frame.
757            reddb_file::page_ownership_journal::encode(record)?;
758            WalRecord::PageOwnership { record }.encode_into(&mut bytes);
759        }
760        for record in records {
761            self.ownership_last_epoch
762                .fetch_max(record.epoch(), Ordering::AcqRel);
763        }
764        self.append_durable_ownership_bytes(bytes)
765    }
766
767    fn append_durable_ownership_bytes(&self, bytes: Vec<u8>) -> io::Result<()> {
768        let _epoch = self.queue.epoch.read();
769        // Set before writing: a failed or uncertain append must never allow
770        // checkpoint truncation to discard the ownership journal.
771        self.ownership_wal_state.store(2, Ordering::Release);
772        if matches!(self.mode, DurabilityMode::Strict) {
773            self.wal.lock().append_bytes(&bytes)?;
774            return self.force_sync();
775        }
776
777        let length = bytes.len() as u64;
778        let target = self.queue.enqueue(bytes);
779        let (state_lock, cond) = &*self.state;
780        let mut state = state_lock.lock();
781        state.pending_target_lsn = state.pending_target_lsn.max(target);
782        state.pending_statements = state.pending_statements.saturating_add(1);
783        state.pending_wal_bytes = state.pending_wal_bytes.saturating_add(length);
784        state.first_pending_at.get_or_insert_with(Instant::now);
785        cond.notify_all();
786        loop {
787            if let Some(error) = state.last_error.clone() {
788                return Err(io::Error::other(error));
789            }
790            if state.durable_lsn >= target {
791                return Ok(());
792            }
793            cond.wait(&mut state);
794        }
795    }
796
797    pub(crate) fn force_sync(&self) -> io::Result<()> {
798        // Strict append already owns a read lease. Do not wait behind a
799        // checkpoint that needs that append to finish before resetting.
800        let _epoch = self.queue.epoch.read_recursive();
801        {
802            let mut wal = self.wal.lock();
803            wal.sync()?;
804            self.fsync_count.fetch_add(1, Ordering::Relaxed);
805            let durable = wal.durable_lsn();
806            drop(wal);
807            let (state_lock, cond) = &*self.state;
808            let mut state = state_lock.lock();
809            state.durable_lsn = durable;
810            state.pending_target_lsn = durable.max(state.pending_target_lsn);
811            state.pending_statements = 0;
812            state.pending_wal_bytes = 0;
813            state.first_pending_at = None;
814            state.last_error = None;
815            cond.notify_all();
816        }
817        Ok(())
818    }
819
820    pub(crate) fn truncate(&self) -> io::Result<()> {
821        self.truncate_inner(false)
822    }
823
824    pub(super) fn truncate_after_runtime_checkpoint(&self) -> io::Result<()> {
825        self.truncate_inner(true)
826    }
827
828    fn truncate_inner(&self, runtime_snapshots_durable: bool) -> io::Result<()> {
829        let _epoch = self.queue.epoch.write();
830        // Async appenders may have returned while their entries remain queued.
831        // Drain them before deciding what the checkpoint may discard.
832        {
833            let mut wal = self.wal.lock();
834            for (offset, bytes) in self.queue.drain_sorted() {
835                if offset != wal.current_lsn() {
836                    return Err(io::Error::other("WAL queue offset differs from writer"));
837                }
838                wal.append_bytes(&bytes)?;
839            }
840            wal.sync()?;
841        }
842        if self.ownership_wal_state.load(Ordering::Acquire) == 0 {
843            let reader = WalReader::open(&self.wal_path)?;
844            let mut protected = false;
845            for item in reader.iter() {
846                match item {
847                    Ok((_, WalRecord::PageOwnership { .. })) => {
848                        protected = true;
849                        break;
850                    }
851                    Ok(_) => {}
852                    Err(error) if error.kind() == io::ErrorKind::UnexpectedEof => break,
853                    Err(error) => return Err(error),
854                }
855            }
856            self.ownership_wal_state
857                .store(if protected { 2 } else { 1 }, Ordering::Release);
858        }
859        if self.ownership_wal_state.load(Ordering::Acquire) == 2 {
860            // A Finish record alone cannot prove that both superblock copies
861            // are fenced; only recovery may eventually authorize truncation.
862            return Err(io::Error::other(
863                "cannot truncate WAL while page ownership records require recovery",
864            ));
865        }
866        if self.runtime_wal_state.load(Ordering::Acquire) == 0 {
867            let mut protected = false;
868            for item in WalReader::open(&self.wal_path)?.iter() {
869                if matches!(
870                    item?.1,
871                    WalRecord::ProbabilisticDelta { .. } | WalRecord::VectorInsert { .. }
872                ) {
873                    protected = true;
874                }
875            }
876            self.runtime_wal_state
877                .store(if protected { 2 } else { 1 }, Ordering::Release);
878        }
879        self.wal_generation.fetch_add(1, Ordering::AcqRel);
880        let mut wal = self.wal.lock();
881        let retain_runtime =
882            !runtime_snapshots_durable && self.runtime_wal_state.load(Ordering::Acquire) == 2;
883        if retain_runtime {
884            self.retain_runtime_wal(&mut wal)?;
885        } else {
886            wal.truncate()?;
887            self.runtime_wal_state.store(1, Ordering::Release);
888        }
889        let durable = wal.durable_lsn();
890        let current = wal.current_lsn();
891        drop(wal);
892
893        // Queue's next_lsn tracks byte offsets in the same space as
894        // wal.current_lsn. After truncate both must be reset together
895        // — otherwise enqueue returns a target_lsn in the old range
896        // that drain can never reach, and wait_until_durable hangs.
897        self.queue.reset(current);
898
899        let (state_lock, cond) = &*self.state;
900        let mut state = state_lock.lock();
901        state.durable_lsn = durable;
902        state.pending_target_lsn = durable;
903        state.pending_statements = 0;
904        state.pending_wal_bytes = 0;
905        state.first_pending_at = None;
906        state.last_error = None;
907        cond.notify_all();
908        drop(state);
909        if retain_runtime {
910            use reddb_file::Vfs;
911            let parent = self
912                .wal_path
913                .parent()
914                .filter(|p| !p.as_os_str().is_empty())
915                .unwrap_or_else(|| Path::new("."));
916            reddb_file::StdVfs.sync_dir(parent)?;
917        }
918        Ok(())
919    }
920
921    /// Replace the checkpointed WAL with the exact runtime-owned frames. The
922    /// temporary file is synced before atomic publication, so interruption
923    /// leaves either the original WAL or a complete retained journal.
924    fn retain_runtime_wal(&self, wal: &mut WalWriter) -> io::Result<()> {
925        use std::io::{Read, Seek, SeekFrom, Write};
926        static NEXT_TEMP: AtomicU64 = AtomicU64::new(0);
927        let mut name = self
928            .wal_path
929            .file_name()
930            .ok_or_else(|| io::Error::other("WAL filename missing"))?
931            .to_os_string();
932        name.push(format!(
933            ".{}.{}.runtime.tmp",
934            std::process::id(),
935            NEXT_TEMP.fetch_add(1, Ordering::Relaxed)
936        ));
937        let temporary = self.wal_path.with_file_name(name);
938        let mut options = std::fs::OpenOptions::new();
939        options.write(true).create_new(true);
940        #[cfg(unix)]
941        {
942            use std::os::unix::fs::OpenOptionsExt;
943            options.mode(0o600);
944        }
945        let mut temp_file = options.open(&temporary)?;
946        let result = (|| -> io::Result<()> {
947            let mut source = std::fs::File::open(&self.wal_path)?;
948            let mut header = [0u8; reddb_file::WAL_FILE_HEADER_BYTES];
949            source.read_exact(&mut header)?;
950            reddb_file::decode_wal_file_header(&header)
951                .map_err(|error| io::Error::other(error.to_string()))?;
952            temp_file.write_all(&header)?;
953            temp_file.sync_all()?;
954            drop(temp_file);
955            let mut retained = WalWriter::open(&temporary)?;
956            let mut previous = None;
957            let end = wal.current_lsn();
958            let mut buffer = [0u8; 64 * 1024];
959            let mut copy_frame = |start: u64, end: u64| -> io::Result<()> {
960                source.seek(SeekFrom::Start(start))?;
961                let mut remaining = end
962                    .checked_sub(start)
963                    .ok_or_else(|| io::Error::other("invalid WAL frame boundary"))?;
964                while remaining > 0 {
965                    let length = usize::try_from(remaining.min(buffer.len() as u64))
966                        .map_err(|_| io::Error::other("invalid WAL frame length"))?;
967                    source.read_exact(&mut buffer[..length])?;
968                    retained.append_bytes(&buffer[..length])?;
969                    remaining -= length as u64;
970                }
971                Ok(())
972            };
973            for item in WalReader::open(&self.wal_path)?.iter() {
974                let (offset, record) = item?;
975                if let Some((start, true)) = previous.take() {
976                    copy_frame(start, offset)?;
977                }
978                previous = Some((
979                    offset,
980                    matches!(
981                        record,
982                        WalRecord::ProbabilisticDelta { .. } | WalRecord::VectorInsert { .. }
983                    ),
984                ));
985            }
986            if let Some((start, true)) = previous {
987                copy_frame(start, end)?;
988            }
989            drop(source);
990            retained.sync()?;
991            #[cfg(test)]
992            if std::env::var_os("REDDB_TEST_EXIT_BEFORE_RUNTIME_WAL_RENAME").is_some() {
993                std::process::exit(211);
994            }
995            std::fs::rename(&temporary, &self.wal_path)?;
996            #[cfg(test)]
997            if std::env::var_os("REDDB_TEST_EXIT_AFTER_RUNTIME_WAL_RENAME").is_some() {
998                std::process::exit(212);
999            }
1000            *wal = retained;
1001            Ok(())
1002        })();
1003        if result.is_err() {
1004            let _ = std::fs::remove_file(&temporary);
1005        }
1006        result
1007    }
1008
1009    fn release_completed_ownership_wal(&self) -> io::Result<()> {
1010        use reddb_file::page_ownership_journal::{encode, pending_epoch, PageOwnershipRecord};
1011        let _epoch = self.queue.epoch.write();
1012        let mut journal = Vec::new();
1013        let mut last = None;
1014        for item in WalReader::open(&self.wal_path)?.iter() {
1015            match item {
1016                Ok((_, WalRecord::PageOwnership { record })) => {
1017                    journal.extend_from_slice(&encode(record)?);
1018                    last = Some(record);
1019                }
1020                Ok(_) => {}
1021                Err(error) if error.kind() == io::ErrorKind::UnexpectedEof => {
1022                    return Err(io::Error::new(
1023                        io::ErrorKind::InvalidData,
1024                        "incomplete WAL before ownership completion",
1025                    ));
1026                }
1027                Err(error) => return Err(error),
1028            }
1029        }
1030        if pending_epoch(&journal)?.is_some()
1031            || !matches!(last, Some(PageOwnershipRecord::Finish { .. }))
1032        {
1033            return Err(io::Error::other("page ownership WAL is not complete"));
1034        }
1035        self.ownership_wal_state.store(1, Ordering::Release);
1036        Ok(())
1037    }
1038
1039    fn recover_page_ownership(&self, store: &UnifiedStore, journal: &[u8]) -> io::Result<()> {
1040        use reddb_file::page_ownership_journal::{
1041            pending_epoch, recovery_plan, PageOwnershipRecord,
1042        };
1043        if journal.is_empty() {
1044            return Ok(());
1045        }
1046        let pager = store
1047            .pager
1048            .as_ref()
1049            .ok_or_else(|| io::Error::other("ownership WAL requires a pager"))?;
1050        let header = pager
1051            .header()
1052            .map_err(|error| io::Error::other(error.to_string()))?;
1053        if header.version < reddb_file::file_format::PAGE_OWNERSHIP_FILE_VERSION {
1054            return Err(io::Error::other(
1055                "page ownership journal requires recovery with a fenced file version",
1056            ));
1057        }
1058        let pending = pending_epoch(journal).map_err(|error| {
1059            io::Error::other(format!("page ownership journal requires recovery: {error}"))
1060        })?;
1061        if let Some(mut pending) = pending {
1062            let catalog_epoch = store.ownership_catalog_epoch.load(Ordering::Acquire);
1063            let (_, retained_pages) = pager
1064                .deferred_reclamation_pages()
1065                .map_err(|error| io::Error::other(error.to_string()))?;
1066            // A retirement intent precedes the catalog's deferred-free ledger.
1067            // Publication does not release pages that this ledger still owns,
1068            // including pages allocated and retired within the same epoch.
1069            if catalog_epoch == pending.epoch {
1070                for page in &retained_pages {
1071                    pending.retired_pages.remove(page);
1072                }
1073            }
1074            let mut plan = recovery_plan(
1075                &pending,
1076                catalog_epoch,
1077                reddb_engine::storage::pager_io::FIRST_ALLOCATABLE_PAGE,
1078                header.page_count,
1079            )?;
1080            if retained_pages
1081                .iter()
1082                .any(|page| plan.release.binary_search(page).is_ok())
1083            {
1084                return Err(io::Error::other(
1085                    "ownership recovery would release a catalog-retained page",
1086                ));
1087            }
1088            plan.preserve.extend(retained_pages);
1089            plan.preserve.sort_unstable();
1090            plan.preserve.dedup();
1091            pager
1092                .validate_owned_pages(&plan.preserve)
1093                .map_err(|error| io::Error::other(error.to_string()))?;
1094            pager
1095                .recover_unowned_pages(&plan.release)
1096                .map_err(|error| io::Error::other(error.to_string()))?;
1097            #[cfg(test)]
1098            if std::env::var_os("REDDB_EXPERIMENTAL_EXIT_DURING_OWNERSHIP_RECOVERY").is_some() {
1099                std::process::exit(187);
1100            }
1101            pager
1102                .sync_recovered_allocator()
1103                .map_err(|error| io::Error::other(error.to_string()))?;
1104            #[cfg(test)]
1105            if std::env::var_os("REDDB_EXPERIMENTAL_EXIT_AFTER_OWNERSHIP_SYNC").is_some() {
1106                std::process::exit(188);
1107            }
1108            self.append_durable_page_ownership(PageOwnershipRecord::Finish {
1109                epoch: pending.epoch,
1110            })?;
1111            #[cfg(test)]
1112            if std::env::var_os("REDDB_EXPERIMENTAL_EXIT_AFTER_OWNERSHIP_FINISH").is_some() {
1113                std::process::exit(189);
1114            }
1115        }
1116        self.release_completed_ownership_wal()
1117    }
1118
1119    pub(super) fn next_page_ownership_epoch(&self, store: &UnifiedStore) -> io::Result<u64> {
1120        self.ownership_last_epoch
1121            .load(Ordering::Acquire)
1122            .max(store.ownership_catalog_epoch.load(Ordering::Acquire))
1123            .checked_add(1)
1124            .ok_or_else(|| io::Error::other("page ownership epoch exhausted"))
1125    }
1126
1127    /// Begin an epoch only when the caller has excluded all other pager mutators.
1128    pub(super) fn begin_page_ownership_epoch(
1129        &self,
1130        store: &UnifiedStore,
1131        epoch: u64,
1132    ) -> io::Result<()> {
1133        if store.ownership_active_epoch.load(Ordering::Acquire) != 0
1134            || epoch <= store.ownership_catalog_epoch.load(Ordering::Acquire)
1135            || epoch <= self.ownership_last_epoch.load(Ordering::Acquire)
1136        {
1137            return Err(io::Error::other("invalid page ownership epoch"));
1138        }
1139        let pager = store
1140            .pager
1141            .as_ref()
1142            .ok_or_else(|| io::Error::other("ownership epoch requires a pager"))?;
1143        let format_version = if !store.secondary_index_specs.read().is_empty() {
1144            reddb_file::file_format::PAGE_SECONDARY_INDEX_FILE_VERSION
1145        } else if store.config.resident_memory_bytes_max.is_some() {
1146            reddb_file::file_format::PAGE_DISK_RESIDENCY_FILE_VERSION
1147        } else {
1148            reddb_file::file_format::PAGE_CHECKPOINT_UNDO_FILE_VERSION
1149        };
1150        // Dropping the final secondary tree never permits an old writer to
1151        // reopen the file; its retired pages may still be reader-pinned.
1152        let format_version = format_version.max(
1153            pager
1154                .header()
1155                .map_err(|error| io::Error::other(error.to_string()))?
1156                .version,
1157        );
1158        pager
1159            .fence_file_format_version(format_version)
1160            .map_err(|error| io::Error::other(error.to_string()))?;
1161        let coordinator = Arc::clone(
1162            store
1163                .commit
1164                .as_ref()
1165                .ok_or_else(|| io::Error::other("ownership epoch requires a WAL coordinator"))?,
1166        );
1167        let recorder = Arc::new(StoreOwnershipRecorder::begin(coordinator, epoch)?);
1168        pager.install_ownership_recorder(recorder)?;
1169        store.ownership_active_epoch.store(epoch, Ordering::Release);
1170        Ok(())
1171    }
1172
1173    /// Complete the epoch only while the same exclusion remains in force.
1174    pub(super) fn finish_page_ownership_epoch(
1175        &self,
1176        store: &UnifiedStore,
1177        epoch: u64,
1178    ) -> io::Result<()> {
1179        if epoch == 0 || store.ownership_active_epoch.load(Ordering::Acquire) != epoch {
1180            return Err(io::Error::other("page ownership epoch is not active"));
1181        }
1182        if store.ownership_catalog_epoch.load(Ordering::Acquire) != epoch {
1183            return Err(io::Error::other("page ownership epoch is not published"));
1184        }
1185        let pager = store
1186            .pager
1187            .as_ref()
1188            .ok_or_else(|| io::Error::other("ownership epoch requires a pager"))?;
1189        pager
1190            .sync_recovered_allocator()
1191            .map_err(|error| io::Error::other(error.to_string()))?;
1192        self.append_durable_page_ownership(
1193            reddb_file::page_ownership_journal::PageOwnershipRecord::Finish { epoch },
1194        )?;
1195        pager.clear_ownership_recorder()?;
1196        store.ownership_active_epoch.store(0, Ordering::Release);
1197        self.release_completed_ownership_wal()
1198    }
1199
1200    pub(crate) fn replay_into(&self, store: &UnifiedStore) -> io::Result<()> {
1201        if !self.wal_path.exists() {
1202            return Ok(());
1203        }
1204
1205        let reader = match WalReader::open(&self.wal_path) {
1206            Ok(reader) => reader,
1207            Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(()),
1208            Err(err) => return Err(err),
1209        };
1210
1211        let mut tx_states = std::collections::HashMap::<u64, bool>::new();
1212        let mut has_page_writes = false;
1213        let mut ownership_journal = Vec::new();
1214        let mut latest_ownership_epoch = 0;
1215        let mut incomplete_wal_tail = false;
1216        let mut applied = false;
1217
1218        for record in reader.iter() {
1219            let (_lsn, record) = match record {
1220                Ok(record) => record,
1221                Err(err) if err.kind() == io::ErrorKind::UnexpectedEof => {
1222                    incomplete_wal_tail = true;
1223                    break;
1224                }
1225                Err(err) => return Err(err),
1226            };
1227            match record {
1228                WalRecord::PageOwnership { record } => {
1229                    latest_ownership_epoch = latest_ownership_epoch.max(record.epoch());
1230                    self.ownership_last_epoch
1231                        .fetch_max(record.epoch(), Ordering::AcqRel);
1232                    ownership_journal
1233                        .extend_from_slice(&reddb_file::page_ownership_journal::encode(record)?);
1234                }
1235                WalRecord::TxCommitBatch { actions, .. } => {
1236                    for payload in actions {
1237                        let action = StoreWalAction::decode(&payload)?;
1238                        applied = true;
1239                        store.apply_replayed_action(&action, false).map_err(|err| {
1240                            io::Error::other(format!("failed to replay store wal action: {err}"))
1241                        })?;
1242                        // Retain only the existing bounded exact-row proof.
1243                        // These images came from a complete committed WAL batch;
1244                        // replay need not rebuild a whole tree for a sparse edit.
1245                        store.remember_checkpoint_rows(vec![action], Default::default());
1246                    }
1247                }
1248                WalRecord::Begin { tx_id } => {
1249                    tx_states.insert(tx_id, false);
1250                }
1251                WalRecord::Commit { tx_id } => {
1252                    tx_states.insert(tx_id, true);
1253                }
1254                WalRecord::Rollback { tx_id } => {
1255                    tx_states.remove(&tx_id);
1256                }
1257                WalRecord::PageWrite { .. } => has_page_writes = true,
1258                WalRecord::Checkpoint { .. } => {}
1259                WalRecord::VectorInsert {
1260                    collection,
1261                    entity_id,
1262                    vector,
1263                } => {
1264                    self.runtime_wal_state.store(2, Ordering::Release);
1265                    // Issue #694 — capture `vector.turbo` WAL inserts so the
1266                    // boot-time TurboQuant index rebuild can drain them in
1267                    // WAL order (deterministic against the pre-restart
1268                    // state under a fixed codec seed). The legacy `vector`
1269                    // path doesn't read this map.
1270                    let mut map = store.replayed_turbo_inserts.lock();
1271                    map.entry(
1272                        reddb_engine::storage::system_collection_names::canonical_owned(collection),
1273                    )
1274                    .or_default()
1275                    .push((entity_id, vector));
1276                }
1277                WalRecord::ProbabilisticDelta {
1278                    kind,
1279                    operation,
1280                    name,
1281                    operands,
1282                } => {
1283                    self.runtime_wal_state.store(2, Ordering::Release);
1284                    let mut deltas = store.replayed_probabilistic_deltas.lock();
1285                    deltas.push((kind, operation, name, operands));
1286                }
1287                WalRecord::FullPageImage { .. } => {
1288                    // Pager-level FPI (gh-478); store replay ignores.
1289                }
1290            }
1291        }
1292
1293        if incomplete_wal_tail && !ownership_journal.is_empty() {
1294            return Err(io::Error::new(
1295                io::ErrorKind::InvalidData,
1296                "incomplete WAL prevents ownership recovery",
1297            ));
1298        }
1299        self.recover_page_ownership(store, &ownership_journal)?;
1300
1301        if has_page_writes {
1302            // Do not retain every transaction's page payload while scanning a
1303            // large WAL. The writer remains open and ownership recovery may
1304            // append a Finish record, but it does not truncate the journal.
1305            for item in WalReader::open(&self.wal_path)?.iter() {
1306                let record = match item {
1307                    Ok((_, record)) => record,
1308                    Err(err) if err.kind() == io::ErrorKind::UnexpectedEof => break,
1309                    Err(err) => return Err(err),
1310                };
1311                let WalRecord::PageWrite { tx_id, data, .. } = record else {
1312                    continue;
1313                };
1314                if !tx_states.get(&tx_id).copied().unwrap_or(false) {
1315                    continue;
1316                }
1317                let action = StoreWalAction::decode(&data)?;
1318                applied = true;
1319                store.apply_replayed_action(&action, false).map_err(|err| {
1320                    io::Error::other(format!("failed to replay store wal action: {err}"))
1321                })?;
1322                store.remember_checkpoint_rows(vec![action], Default::default());
1323            }
1324        }
1325
1326        if applied && store.config.resident_memory_bytes_max.is_some() {
1327            // The immutable base plus committed WAL overlay is authoritative.
1328            // Keep the WAL until a paced checkpoint publishes the replacement;
1329            // boot must not copy every cold payload for a few replayed rows.
1330            return Ok(());
1331        }
1332        if applied {
1333            // Opening is exclusive: no client can mutate published trees
1334            // while replay constructs replacement generations.
1335            let epoch = latest_ownership_epoch
1336                .max(store.ownership_catalog_epoch.load(Ordering::Acquire))
1337                .checked_add(1)
1338                .ok_or_else(|| io::Error::other("page ownership epoch exhausted"))?;
1339            self.begin_page_ownership_epoch(store, epoch)?;
1340            // Even when the replayed actions leave all B-trees unchanged, the
1341            // epoch must be in the catalog before its journal can finish.
1342            store.mark_paged_registry_dirty();
1343            store
1344                .reconcile_replayed_btrees()
1345                .map_err(|err| io::Error::other(err.to_string()))?;
1346            self.finish_page_ownership_epoch(store, epoch)?;
1347        }
1348        Ok(())
1349    }
1350
1351    fn wait_until_durable(&self, target_lsn: u64, wal_bytes: u64) -> io::Result<()> {
1352        match self.mode {
1353            DurabilityMode::Strict => self.force_sync(),
1354            // Async: record the pending target so the background
1355            // flusher eventually covers it, but don't block the
1356            // caller. Matches PG `synchronous_commit=off` semantics —
1357            // crash inside the flush window loses unflushed commits.
1358            DurabilityMode::Async => {
1359                let (state_lock, cond) = &*self.state;
1360                let mut state = state_lock.lock();
1361                state.pending_target_lsn = state.pending_target_lsn.max(target_lsn);
1362                state.pending_statements = state.pending_statements.saturating_add(1);
1363                state.pending_wal_bytes = state.pending_wal_bytes.saturating_add(wal_bytes);
1364                state.first_pending_at.get_or_insert_with(Instant::now);
1365                cond.notify_all();
1366                Ok(())
1367            }
1368            DurabilityMode::WalDurableGrouped => {
1369                let (state_lock, cond) = &*self.state;
1370                let mut state = state_lock.lock();
1371                state.pending_target_lsn = state.pending_target_lsn.max(target_lsn);
1372                state.pending_statements = state.pending_statements.saturating_add(1);
1373                state.pending_wal_bytes = state.pending_wal_bytes.saturating_add(wal_bytes);
1374                state.first_pending_at.get_or_insert_with(Instant::now);
1375                cond.notify_all();
1376
1377                loop {
1378                    if let Some(err) = state.last_error.clone() {
1379                        return Err(io::Error::other(err));
1380                    }
1381                    if state.durable_lsn >= target_lsn {
1382                        return Ok(());
1383                    }
1384                    // parking_lot::Condvar mutates the guard in place —
1385                    // no LockResult to unwrap, no poisoning to fold.
1386                    cond.wait(&mut state);
1387                }
1388            }
1389        }
1390    }
1391
1392    fn run_group_commit_loop(
1393        wal: Arc<WalMutex>,
1394        queue: Arc<WalAppendQueue>,
1395        state: Arc<(CommitStateMutex, CommitStateCondvar)>,
1396        fsync_count: Arc<AtomicU64>,
1397        window: Duration,
1398        max_statements: usize,
1399        max_wal_bytes: u64,
1400    ) {
1401        let (state_lock, cond) = &*state;
1402        loop {
1403            let target_lsn = {
1404                let mut guard = state_lock.lock();
1405
1406                while !guard.shutdown && guard.pending_target_lsn <= guard.durable_lsn {
1407                    cond.wait(&mut guard);
1408                }
1409
1410                if guard.shutdown {
1411                    return;
1412                }
1413
1414                let immediate = window.is_zero()
1415                    || guard.pending_statements >= max_statements
1416                    || guard.pending_wal_bytes >= max_wal_bytes;
1417
1418                if !immediate {
1419                    let deadline = guard.first_pending_at.unwrap_or_else(Instant::now) + window;
1420                    let now = Instant::now();
1421                    if now < deadline {
1422                        let timeout = deadline.saturating_duration_since(now);
1423                        let _ = cond.wait_for(&mut guard, timeout);
1424                        if guard.shutdown {
1425                            return;
1426                        }
1427                        if guard.pending_target_lsn <= guard.durable_lsn {
1428                            continue;
1429                        }
1430                        let should_wait_again = guard.pending_statements < max_statements
1431                            && guard.pending_wal_bytes < max_wal_bytes
1432                            && guard
1433                                .first_pending_at
1434                                .map(|first| first.elapsed() < window)
1435                                .unwrap_or(false);
1436                        if should_wait_again {
1437                            continue;
1438                        }
1439                    }
1440                }
1441
1442                guard.pending_target_lsn
1443            };
1444
1445            // Drain the current epoch even when reset is waiting: outstanding
1446            // append leases cannot finish until this flusher acknowledges them.
1447            // Retain the lease through state publication to exclude stale ACKs
1448            // after a checkpoint resets offsets back to the WAL header.
1449            let _epoch = queue.epoch.read_recursive();
1450
1451            // Drain all queued entries. Since `WalAppendQueue::enqueue`
1452            // assigns LSN and pushes under a single mutex, the drained
1453            // tuples are guaranteed to form a contiguous byte range
1454            // starting at `wal.current_lsn()` — no gaps, no leftover
1455            // handling.
1456            let batches = queue.drain_sorted();
1457
1458            let sync_result = {
1459                let mut wal = wal.lock();
1460                let mut write_err: Option<io::Error> = None;
1461                for (_lsn, bytes) in batches {
1462                    if let Err(e) = wal.append_bytes(&bytes) {
1463                        write_err = Some(e);
1464                        break;
1465                    }
1466                }
1467                match write_err {
1468                    Some(e) => Err(e),
1469                    None => wal.sync().map(|_| {
1470                        // Count the fsync exactly once per drain
1471                        // cycle so tests can compare it against the
1472                        // number of `append_actions` callers that
1473                        // entered the queue.
1474                        fsync_count.fetch_add(1, Ordering::Relaxed);
1475                        wal.durable_lsn()
1476                    }),
1477                }
1478            };
1479
1480            // Late enqueuers that arrived after our drain stay in the
1481            // queue — don't clear the `pending_*` counters yet and
1482            // don't claim we reached `target_lsn` unless the fsync
1483            // actually covers it.
1484            let more_pending = queue.has_pending();
1485
1486            let mut guard = state_lock.lock();
1487            match sync_result {
1488                Ok(durable_lsn) => {
1489                    guard.durable_lsn = durable_lsn;
1490                    if !more_pending {
1491                        guard.pending_statements = 0;
1492                        guard.pending_wal_bytes = 0;
1493                        guard.first_pending_at = None;
1494                    }
1495                    guard.last_error = None;
1496                    let _ = target_lsn;
1497                }
1498                Err(err) => {
1499                    guard.last_error = Some(err.to_string());
1500                }
1501            }
1502            cond.notify_all();
1503        }
1504    }
1505}
1506
1507impl Drop for StoreCommitCoordinator {
1508    fn drop(&mut self) {
1509        let (state_lock, cond) = &*self.state;
1510        // parking_lot::Mutex::lock is infallible (no poisoning).
1511        let mut state = state_lock.lock();
1512        state.shutdown = true;
1513        cond.notify_all();
1514    }
1515}
1516
1517impl UnifiedStore {
1518    pub(crate) fn deferred_store_wal_capture_active() -> bool {
1519        deferred_store_wal_capture_active()
1520    }
1521
1522    pub(crate) fn begin_deferred_store_wal_capture() {
1523        begin_deferred_store_wal_capture();
1524    }
1525
1526    pub(crate) fn take_deferred_store_wal_capture() -> DeferredStoreWalActions {
1527        take_deferred_store_wal_capture()
1528    }
1529
1530    pub(crate) fn append_deferred_store_wal_actions(
1531        &self,
1532        actions: DeferredStoreWalActions,
1533    ) -> Result<(), StoreError> {
1534        if actions.actions.is_empty() {
1535            return Ok(());
1536        }
1537        #[cfg(test)]
1538        if self
1539            .test_require_deferred_publication_fence
1540            .swap(false, Ordering::AcqRel)
1541            && !self.vault_publication_lock.is_owned_by_current_thread()
1542        {
1543            return Err(StoreError::Io(io::Error::other(
1544                "deferred WAL missing checkpoint publication fence",
1545            )));
1546        }
1547        #[cfg(test)]
1548        if self
1549            .test_fail_deferred_wal_append
1550            .swap(false, Ordering::AcqRel)
1551        {
1552            return Err(StoreError::Io(io::Error::other(
1553                "injected deferred WAL append failure",
1554            )));
1555        }
1556        if self.config.embedded_wal_path.is_some() {
1557            return self.append_embedded_store_wal_actions(&actions.actions);
1558        }
1559        // Deferred transactions need a committed WAL batch even in Strict
1560        // mode. WAL-only versions have no pager pages to flush yet.
1561        if let Some(commit) = &self.commit {
1562            let references = commit
1563                .append_checkpoint_actions(&actions.actions)
1564                .map_err(StoreError::Io)?;
1565            self.remember_checkpoint_rows(actions.actions, references);
1566            Ok(())
1567        } else {
1568            self.flush_paged_state()
1569        }
1570    }
1571
1572    /// Emit a [`WalRecord::VectorInsert`] for a `vector.turbo`
1573    /// collection (issue #693). Returns `Ok(())` when no commit
1574    /// coordinator is configured (in-memory mode) so callers don't
1575    /// have to special-case the missing WAL — the in-memory index
1576    /// update remains durable enough for in-memory runtimes by
1577    /// construction.
1578    pub(crate) fn append_vector_insert_record(
1579        &self,
1580        collection: &str,
1581        entity_id: u64,
1582        vector: &[f32],
1583    ) -> std::io::Result<()> {
1584        let Some(commit) = &self.commit else {
1585            return Ok(());
1586        };
1587        let record = WalRecord::VectorInsert {
1588            collection: collection.to_string(),
1589            entity_id,
1590            vector: vector.to_vec(),
1591        };
1592        commit.append_single_record(record)
1593    }
1594
1595    pub(crate) fn append_probabilistic_delta_record(
1596        &self,
1597        kind: u8,
1598        operation: u8,
1599        name: &str,
1600        operands: Vec<Vec<u8>>,
1601    ) -> std::io::Result<()> {
1602        let Some(commit) = &self.commit else {
1603            return Ok(());
1604        };
1605        let record = WalRecord::ProbabilisticDelta {
1606            kind,
1607            operation,
1608            name: name.to_string(),
1609            operands,
1610        };
1611        commit.append_single_record(record)
1612    }
1613
1614    pub(crate) fn finish_paged_write(
1615        &self,
1616        actions: impl IntoIterator<Item = StoreWalAction>,
1617    ) -> Result<(), StoreError> {
1618        self.finish_paged_write_for_projection(
1619            actions,
1620            self.config.resident_memory_bytes_max.is_some(),
1621        )
1622    }
1623
1624    pub(crate) fn finish_paged_write_for_projection(
1625        &self,
1626        actions: impl IntoIterator<Item = StoreWalAction>,
1627        track: bool,
1628    ) -> Result<(), StoreError> {
1629        let actions: Vec<StoreWalAction> = actions.into_iter().collect();
1630        if deferred_store_wal_capture_active() {
1631            let captured = capture_deferred_store_wal_actions(actions);
1632            debug_assert!(captured);
1633            return Ok(());
1634        }
1635        if self.config.embedded_wal_path.is_some() {
1636            return self.append_embedded_store_wal_actions(&actions);
1637        }
1638        let mut references = super::checkpoint_wal::CheckpointWalBatch::default();
1639        let result = match self.config.durability_mode {
1640            DurabilityMode::Strict => {
1641                if let Some(commit) = &self.commit {
1642                    if track {
1643                        references = commit
1644                            .append_checkpoint_actions(&actions)
1645                            .map_err(StoreError::Io)?;
1646                    } else {
1647                        commit.append_actions(&actions).map_err(StoreError::Io)?;
1648                    }
1649                }
1650                self.flush_paged_state()
1651            }
1652            DurabilityMode::WalDurableGrouped | DurabilityMode::Async => {
1653                if let Some(commit) = &self.commit {
1654                    if track {
1655                        references = commit
1656                            .append_checkpoint_actions(&actions)
1657                            .map_err(StoreError::Io)?;
1658                    } else {
1659                        commit.append_actions(&actions).map_err(StoreError::Io)?;
1660                    }
1661                    Ok(())
1662                } else {
1663                    self.flush_paged_state()
1664                }
1665            }
1666        };
1667        result?;
1668        if track {
1669            self.remember_checkpoint_rows(actions, references);
1670        }
1671        Ok(())
1672    }
1673
1674    pub(crate) fn apply_encoded_store_wal_action(&self, payload: &[u8]) -> Result<(), StoreError> {
1675        let action = StoreWalAction::decode(payload).map_err(StoreError::Io)?;
1676        self.apply_replayed_action(&action, true)
1677    }
1678
1679    fn append_embedded_store_wal_actions(
1680        &self,
1681        actions: &[StoreWalAction],
1682    ) -> Result<(), StoreError> {
1683        let Some(path) = self.config.embedded_wal_path.as_deref() else {
1684            return Ok(());
1685        };
1686        if actions.is_empty() {
1687            return Ok(());
1688        }
1689        let payloads: Vec<Vec<u8>> = actions.iter().map(StoreWalAction::encode).collect();
1690        let encoded_len = crate::storage::EmbeddedRdbArtifact::wal_payloads_encoded_len(&payloads)
1691            .map_err(|err| StoreError::Io(std::io::Error::other(err.to_string())))?;
1692        match self.append_embedded_payloads(path, &payloads) {
1693            Ok(_) => Ok(()),
1694            Err(crate::api::RedDBError::InvalidOperation(msg))
1695                if msg.contains("embedded wal region full") =>
1696            {
1697                self.recover_full_embedded_wal(path, &payloads, encoded_len)
1698            }
1699            Err(err) => Err(StoreError::Io(std::io::Error::other(err.to_string()))),
1700        }
1701    }
1702
1703    fn recover_full_embedded_wal(
1704        &self,
1705        path: &Path,
1706        payloads: &[Vec<u8>],
1707        encoded_len: u64,
1708    ) -> Result<(), StoreError> {
1709        let _vault_guard = self.vault_publication_lock.lock();
1710        // The failed append preceded this lock: another writer may already
1711        // have reclaimed space. Retry through the same FIFO coordinator so
1712        // stale full results cannot each serialize another complete snapshot.
1713        match self.append_embedded_payloads(path, payloads) {
1714            Ok(_) => return Ok(()),
1715            Err(crate::api::RedDBError::InvalidOperation(message))
1716                if message.contains("embedded wal region full") => {}
1717            Err(error) => return Err(StoreError::Io(std::io::Error::other(error.to_string()))),
1718        }
1719        // A checkpoint can fail after publishing its image. Preserve the
1720        // session failure fence through errors and unwinding, just as append's
1721        // FlushBatch guard does; in-memory rollback alone cannot resolve disk.
1722        struct CheckpointFence<'a>(Option<&'a crate::storage::embedded::EmbeddedCommitSession>);
1723        impl Drop for CheckpointFence<'_> {
1724            fn drop(&mut self) {
1725                if let Some(session) = self.0 {
1726                    session.fence_interrupted_write();
1727                }
1728            }
1729        }
1730        let mut fence = CheckpointFence(Some(
1731            self.embedded_commit_session
1732                .get()
1733                .expect("WAL-full recovery follows an append attempt"),
1734        ));
1735        let started = Instant::now();
1736        let snapshot = self.to_binary_dump_bytes();
1737        let capacity = encoded_len.max((snapshot.len() as u64).min(EMBEDDED_WAL_HEADROOM_LIMIT));
1738        let checkpoint = crate::storage::EmbeddedRdbArtifact::write_snapshot_with_wal_capacity(
1739            path, &snapshot, capacity,
1740        )
1741        .map_err(|err| StoreError::Io(std::io::Error::other(err.to_string())))?;
1742        fence.0 = None;
1743        tracing::debug!(
1744            target: "reddb::embedded_checkpoint",
1745            snapshot_bytes = snapshot.len(),
1746            wal_region_bytes = checkpoint.manifest.wal_region_bytes,
1747            elapsed_us = started.elapsed().as_micros() as u64,
1748            "embedded WAL overflow checkpoint"
1749        );
1750        self.append_embedded_payloads(path, payloads)
1751            .map(|_| ())
1752            .map_err(|err| StoreError::Io(std::io::Error::other(err.to_string())))
1753    }
1754
1755    fn append_embedded_payloads(
1756        &self,
1757        path: &Path,
1758        payloads: &[Vec<u8>],
1759    ) -> crate::RedDBResult<crate::storage::EmbeddedRdbOpen> {
1760        let session = self
1761            .embedded_commit_session
1762            .get_or_init(|| crate::storage::EmbeddedRdbArtifact::group_commit_session(path));
1763        let options = self.config.group_commit;
1764        let grouped = self.config.durability_mode == DurabilityMode::WalDurableGrouped;
1765        let window = if grouped {
1766            StoreCommitCoordinator::resolve_window(&options)
1767        } else {
1768            Duration::ZERO
1769        };
1770        session.append(
1771            payloads,
1772            reddb_file::embedded::EmbeddedGroupCommitOptions {
1773                window,
1774                max_statements: if grouped { options.max_statements } else { 1 },
1775                max_wal_bytes: options.max_wal_bytes,
1776            },
1777        )
1778    }
1779
1780    pub(super) fn enqueue_grouped_actions(
1781        &self,
1782        actions: &[StoreWalAction],
1783    ) -> Result<Option<crate::storage::embedded::PendingEmbeddedAppend>, StoreError> {
1784        if self.config.durability_mode != DurabilityMode::WalDurableGrouped
1785            || deferred_store_wal_capture_active()
1786        {
1787            return Ok(None);
1788        }
1789        let Some(path) = self.config.embedded_wal_path.as_deref() else {
1790            return Ok(None);
1791        };
1792        let session = self
1793            .embedded_commit_session
1794            .get_or_init(|| crate::storage::EmbeddedRdbArtifact::group_commit_session(path));
1795        let payloads: Vec<Vec<u8>> = actions.iter().map(StoreWalAction::encode).collect();
1796        let options = self.config.group_commit;
1797        session
1798            .enqueue(
1799                &payloads,
1800                reddb_file::embedded::EmbeddedGroupCommitOptions {
1801                    window: StoreCommitCoordinator::resolve_window(&options),
1802                    max_statements: options.max_statements,
1803                    max_wal_bytes: options.max_wal_bytes,
1804                },
1805            )
1806            .map(Some)
1807            .map_err(|error| StoreError::Io(io::Error::other(error.to_string())))
1808    }
1809
1810    pub(super) fn complete_grouped_actions(
1811        &self,
1812        pending: &crate::storage::embedded::PendingEmbeddedAppend,
1813        actions: &[StoreWalAction],
1814    ) -> Result<(), StoreError> {
1815        match pending.wait() {
1816            Ok(_) => Ok(()),
1817            Err(crate::RedDBError::InvalidOperation(message))
1818                if message.contains("embedded wal region full") =>
1819            {
1820                self.append_embedded_store_wal_actions(actions)
1821            }
1822            Err(error) => Err(StoreError::Io(io::Error::other(error.to_string()))),
1823        }
1824    }
1825
1826    pub(crate) fn apply_replayed_action(
1827        &self,
1828        action: &StoreWalAction,
1829        update_btrees: bool,
1830    ) -> Result<(), StoreError> {
1831        reddb_engine::storage::store_wal::apply_replayed_action(
1832            &NativeReplayTarget {
1833                store: self,
1834                update_btrees,
1835            },
1836            action,
1837        )
1838    }
1839
1840    /// Atomic full-collection replace — issue #595 slice 9c.
1841    ///
1842    /// Builds a fresh `SegmentManager` populated with `entities`,
1843    /// atomically swaps it into the collections map, rebuilds the
1844    /// paged B-tree from the same records, and emits a single
1845    /// `RefreshCollection` WAL action.
1846    ///
1847    /// Concurrent readers that resolved an `Arc<SegmentManager>` for
1848    /// this collection before the swap continue reading the prior
1849    /// contents; readers after the swap see the new contents. The
1850    /// transition is single-pointer so partial state is unobservable.
1851    ///
1852    /// A crash between in-memory mutation and WAL fsync leaves the
1853    /// previous WAL stream intact — on recovery the prior
1854    /// `RefreshCollection` (or whatever last bulk-applied to the
1855    /// collection) is replayed, so the prior contents are observed.
1856    pub fn refresh_collection(
1857        &self,
1858        name: &str,
1859        entities: Vec<UnifiedEntity>,
1860    ) -> Result<Vec<Vec<u8>>, StoreError> {
1861        let name = Self::canonical_collection(name);
1862        let fv = self.format_version();
1863
1864        // Build a fresh manager. Done outside the collections lock so
1865        // the critical section is just the pointer swap below.
1866        let new_manager = Arc::new(SegmentManager::with_config(
1867            name,
1868            self.config.manager_config.clone(),
1869        ));
1870
1871        let mut prepared = entities;
1872        for entity in &mut prepared {
1873            entity.canonicalize_system_collection_names();
1874            if entity.id.raw() == 0 {
1875                entity.id = self.next_entity_id();
1876            } else {
1877                self.register_entity_id(entity.id);
1878            }
1879            if let EntityKind::TableRow { ref mut row_id, .. } = entity.kind {
1880                if *row_id == 0 {
1881                    *row_id = new_manager.next_row_id();
1882                } else {
1883                    new_manager.register_row_id(*row_id);
1884                }
1885            }
1886            entity.ensure_table_logical_id();
1887        }
1888
1889        let serialized: Vec<Vec<u8>> = prepared
1890            .iter()
1891            .map(|e| Self::serialize_entity_record(e, None, fv))
1892            .collect();
1893
1894        new_manager.bulk_insert(prepared.clone()).map_err(|e| {
1895            StoreError::Io(std::io::Error::other(format!(
1896                "refresh_collection: bulk_insert into new manager failed: {e}"
1897            )))
1898        })?;
1899
1900        self.swap_collection_state(name, new_manager, &prepared, &serialized, true);
1901
1902        self.finish_paged_write([StoreWalAction::RefreshCollection {
1903            collection: name.to_string(),
1904            records: serialized.clone(),
1905        }])?;
1906
1907        Ok(serialized)
1908    }
1909
1910    /// Atomic swap of the in-memory collection state. Used by both
1911    /// the live `refresh_collection` path and WAL replay. The new
1912    /// manager is already populated; this fn replaces the live Arc,
1913    /// purges side-state pointing at the previous contents, and
1914    /// rebuilds the paged B-tree from the supplied records.
1915    fn swap_collection_state(
1916        &self,
1917        name: &str,
1918        new_manager: Arc<SegmentManager>,
1919        prepared: &[UnifiedEntity],
1920        serialized: &[Vec<u8>],
1921        update_btrees: bool,
1922    ) {
1923        self.resident
1924            .swap_collection_state(name, new_manager, prepared);
1925
1926        if let Some(pager) = self
1927            .pager
1928            .as_ref()
1929            .filter(|_| update_btrees && self.config.resident_memory_bytes_max.is_none())
1930        {
1931            let new_btree = Arc::new(BTree::new(Arc::clone(pager)));
1932            let mut sorted: Vec<(Vec<u8>, Vec<u8>)> = prepared
1933                .iter()
1934                .zip(serialized.iter())
1935                .map(|(e, r)| (e.id.raw().to_be_bytes().to_vec(), r.clone()))
1936                .collect();
1937            sorted.sort_by(|a, b| a.0.cmp(&b.0));
1938            if !sorted.is_empty() {
1939                let _ = new_btree.bulk_insert_sorted(&sorted);
1940            }
1941            self.btree_indices
1942                .write()
1943                .insert(name.to_string(), new_btree);
1944            self.mark_paged_registry_dirty();
1945        }
1946    }
1947
1948    fn apply_replayed_refresh_collection(
1949        &self,
1950        collection: &str,
1951        records: &[Vec<u8>],
1952        update_btrees: bool,
1953    ) -> Result<(), StoreError> {
1954        let collection = Self::canonical_collection(collection);
1955        let (new_manager, prepared) = self.resident.prepare_replayed_refresh_collection(
1956            collection,
1957            records,
1958            self.format_version(),
1959            &self.config.manager_config,
1960        )?;
1961        self.swap_collection_state(collection, new_manager, &prepared, records, update_btrees);
1962        Ok(())
1963    }
1964
1965    /// Replica-side analogue of [`Self::refresh_collection`] — issue
1966    /// #596 slice 9d. Takes pre-serialized record bytes from the
1967    /// primary's CDC stream (the same bytes the primary wrote into the
1968    /// `RefreshCollection` WAL action), applies the atomic swap, and
1969    /// emits the same `RefreshCollection` WAL action against the
1970    /// replica's local store WAL so the post-swap state survives a
1971    /// replica restart through the normal recovery path.
1972    ///
1973    /// Idempotency: re-applying the same records bytes is a full
1974    /// rebuild from the same payload, so the resulting backing-
1975    /// collection contents are equal to the prior call's result.
1976    /// The primary-side `LogicalChangeApplier` short-circuits exact
1977    /// duplicate LSN+payload combinations before reaching here.
1978    pub fn refresh_collection_from_records(
1979        &self,
1980        name: &str,
1981        records: Vec<Vec<u8>>,
1982    ) -> Result<(), StoreError> {
1983        let name = Self::canonical_collection(name);
1984        self.apply_replayed_refresh_collection(name, &records, true)?;
1985        self.finish_paged_write([StoreWalAction::RefreshCollection {
1986            collection: name.to_string(),
1987            records,
1988        }])?;
1989        Ok(())
1990    }
1991
1992    pub(crate) fn create_collection_in_memory(&self, name: &str) -> Result<(), StoreError> {
1993        let name = Self::canonical_collection(name);
1994        self.resident
1995            .create_collection_in_memory(name, &self.config.manager_config)
1996    }
1997
1998    fn drop_collection_in_memory(&self, name: &str) -> Result<(), StoreError> {
1999        let name = Self::canonical_collection(name);
2000        self.resident.drop_collection_in_memory(name, || {
2001            if let Some(tree) = self.btree_indices.write().remove(name) {
2002                self.retire_btree(tree);
2003            }
2004        })
2005    }
2006
2007    fn apply_replayed_upsert(
2008        &self,
2009        collection: &str,
2010        record: &[u8],
2011        update_btrees: bool,
2012    ) -> Result<(), StoreError> {
2013        let collection = Self::canonical_collection(collection);
2014        let id = self.resident.apply_replayed_upsert(
2015            collection,
2016            record,
2017            self.format_version(),
2018            &self.config.manager_config,
2019        )?;
2020
2021        if let Some(pager) = self
2022            .pager
2023            .as_ref()
2024            .filter(|_| update_btrees && self.config.resident_memory_bytes_max.is_none())
2025        {
2026            let mut btree_indices = self.btree_indices.write();
2027            let btree = btree_indices
2028                .entry(collection.to_string())
2029                .or_insert_with(|| Arc::new(BTree::new(Arc::clone(pager))));
2030            let root_before = btree.root_page_id();
2031            let key = id.raw().to_be_bytes();
2032            match btree.insert(&key, record) {
2033                Ok(_) => {}
2034                Err(BTreeError::DuplicateKey) => {
2035                    let _ = btree.delete(&key);
2036                    let _ = btree.insert(&key, record);
2037                }
2038                Err(err) => {
2039                    return Err(StoreError::Io(io::Error::other(format!(
2040                        "replay upsert btree error: {err}"
2041                    ))));
2042                }
2043            }
2044            if root_before != btree.root_page_id() {
2045                self.mark_paged_registry_dirty();
2046            }
2047        }
2048
2049        Ok(())
2050    }
2051
2052    fn apply_replayed_delete(
2053        &self,
2054        collection: &str,
2055        id: EntityId,
2056        update_btrees: bool,
2057    ) -> Result<(), StoreError> {
2058        let collection = Self::canonical_collection(collection);
2059        self.resident.apply_replayed_delete(collection, id, || {
2060            if self.pager.is_some()
2061                && update_btrees
2062                && self.config.resident_memory_bytes_max.is_none()
2063            {
2064                let btree_indices = self.btree_indices.read();
2065                if let Some(btree) = btree_indices.get(collection) {
2066                    let root_before = btree.root_page_id();
2067                    let key = id.raw().to_be_bytes();
2068                    let _ = btree.delete(&key);
2069                    if root_before != btree.root_page_id() {
2070                        self.mark_paged_registry_dirty();
2071                    }
2072                }
2073            }
2074        })
2075    }
2076}
2077
2078#[cfg(all(test, target_os = "linux"))]
2079#[path = "storage_apocalypse.rs"]
2080mod storage_apocalypse;
2081
2082#[cfg(test)]
2083mod tests {
2084    use super::*;
2085    use crate::api::{DurabilityMode, GroupCommitOptions};
2086    use std::sync::{Barrier, Mutex as StdMutex, OnceLock};
2087    use std::time::SystemTime;
2088
2089    #[test]
2090    fn runtime_wal_retention_serializes_concurrent_appends() {
2091        let directory = tempfile::tempdir().expect("temporary WAL directory");
2092        let path = directory.path().join("data.wal");
2093        let coordinator = Arc::new(
2094            StoreCommitCoordinator::open(
2095                &path,
2096                DurabilityMode::WalDurableGrouped,
2097                GroupCommitOptions::default(),
2098            )
2099            .expect("coordinator"),
2100        );
2101        let barrier = Arc::new(Barrier::new(5));
2102        let mut workers = Vec::new();
2103        for worker in 0..4u8 {
2104            let coordinator = Arc::clone(&coordinator);
2105            let barrier = Arc::clone(&barrier);
2106            workers.push(std::thread::spawn(move || {
2107                barrier.wait();
2108                for index in 0..20u8 {
2109                    coordinator
2110                        .append_single_record(WalRecord::ProbabilisticDelta {
2111                            kind: 1,
2112                            operation: 1,
2113                            name: "visitors".into(),
2114                            operands: vec![vec![worker, index]],
2115                        })
2116                        .expect("append delta");
2117                }
2118            }));
2119        }
2120        barrier.wait();
2121        for _ in 0..5 {
2122            coordinator.truncate().expect("retain while appending");
2123        }
2124        for worker in workers {
2125            worker.join().expect("worker");
2126        }
2127        coordinator.truncate().expect("final retention");
2128        let entries: std::collections::HashSet<_> = WalReader::open(&path)
2129            .expect("reader")
2130            .iter()
2131            .map(|entry| match entry.expect("record").1 {
2132                WalRecord::ProbabilisticDelta { operands, .. } => operands[0].clone(),
2133                _ => panic!("unexpected retained record"),
2134            })
2135            .collect();
2136        assert_eq!(entries.len(), 80);
2137    }
2138
2139    #[test]
2140    fn runtime_wal_retention_preserves_exact_frames_and_append_order() {
2141        use crate::storage::wal::record::WalRecordAuthority;
2142        for mode in [
2143            DurabilityMode::Strict,
2144            DurabilityMode::WalDurableGrouped,
2145            DurabilityMode::Async,
2146        ] {
2147            let directory = tempfile::tempdir().expect("temporary WAL directory");
2148            let path = directory.path().join("data.wal");
2149            let record = WalRecord::ProbabilisticDelta {
2150                kind: 1,
2151                operation: 1,
2152                name: "visitors".into(),
2153                operands: vec![b"alice".to_vec()],
2154            };
2155            let mut frame = Vec::new();
2156            record.encode_with_authority_into(
2157                &mut frame,
2158                WalRecordAuthority {
2159                    term: 42,
2160                    ownership_epoch: Some(9),
2161                },
2162            );
2163            {
2164                let mut writer = WalWriter::open(&path).expect("WAL writer");
2165                writer
2166                    .append(&WalRecord::Begin { tx_id: 1 })
2167                    .expect("logical begin");
2168                writer.append_bytes(&frame).expect("runtime record");
2169                writer
2170                    .append(&WalRecord::Commit { tx_id: 1 })
2171                    .expect("logical commit");
2172                writer.sync().expect("sync");
2173            }
2174            let coordinator =
2175                StoreCommitCoordinator::open(&path, mode, GroupCommitOptions::default())
2176                    .expect("coordinator");
2177            coordinator.truncate().expect("retain runtime records");
2178            let mut expected = reddb_file::encode_wal_file_header().to_vec();
2179            expected.extend_from_slice(&frame);
2180            assert_eq!(std::fs::read(&path).expect("retained WAL"), expected);
2181            let later = WalRecord::ProbabilisticDelta {
2182                kind: 1,
2183                operation: 1,
2184                name: "visitors".into(),
2185                operands: vec![b"bob".to_vec()],
2186            };
2187            coordinator
2188                .append_single_record(later.clone())
2189                .expect("append after rewrite");
2190            coordinator.truncate().expect("retain queued record");
2191            let records: Vec<_> = WalReader::open(&path)
2192                .expect("reader")
2193                .iter()
2194                .map(|entry| entry.expect("record").1)
2195                .collect();
2196            assert_eq!(records, vec![record, later]);
2197            coordinator
2198                .truncate_after_runtime_checkpoint()
2199                .expect("runtime snapshot permits truncation");
2200            assert_eq!(
2201                std::fs::read(&path).expect("checkpointed WAL"),
2202                reddb_file::encode_wal_file_header()
2203            );
2204        }
2205    }
2206
2207    #[test]
2208    fn runtime_wal_rewrite_survives_interruption() {
2209        const TEST: &str =
2210            "storage::unified::store::commit::tests::runtime_wal_rewrite_survives_interruption";
2211        const CHILD: &str = "REDDB_TEST_RUNTIME_WAL_REWRITE_PATH";
2212        if let Some(path) = std::env::var_os(CHILD) {
2213            let coordinator = StoreCommitCoordinator::open(
2214                path,
2215                DurabilityMode::Strict,
2216                GroupCommitOptions::default(),
2217            )
2218            .expect("child coordinator");
2219            coordinator.truncate().expect("fault injection");
2220            panic!("interruption point was not reached");
2221        }
2222        for (hook, code, replaced) in [
2223            ("REDDB_TEST_EXIT_BEFORE_RUNTIME_WAL_RENAME", 211, false),
2224            ("REDDB_TEST_EXIT_AFTER_RUNTIME_WAL_RENAME", 212, true),
2225        ] {
2226            let directory = tempfile::tempdir().expect("temporary WAL directory");
2227            let path = directory.path().join("data.wal");
2228            let record = WalRecord::ProbabilisticDelta {
2229                kind: 1,
2230                operation: 1,
2231                name: "visitors".into(),
2232                operands: vec![b"alice".to_vec()],
2233            };
2234            {
2235                let mut writer = WalWriter::open(&path).expect("WAL writer");
2236                writer
2237                    .append(&WalRecord::Begin { tx_id: 1 })
2238                    .expect("begin");
2239                writer.append(&record).expect("runtime record");
2240                writer
2241                    .append(&WalRecord::Commit { tx_id: 1 })
2242                    .expect("commit");
2243                writer.sync().expect("sync");
2244            }
2245            let original = std::fs::read(&path).expect("original WAL");
2246            let child =
2247                std::process::Command::new(std::env::current_exe().expect("test executable"))
2248                    .args(["--exact", TEST, "--nocapture"])
2249                    .env(CHILD, &path)
2250                    .env(hook, "1")
2251                    .output()
2252                    .expect("child");
2253            assert_eq!(
2254                child.status.code(),
2255                Some(code),
2256                "{}",
2257                String::from_utf8_lossy(&child.stderr)
2258            );
2259            let after = std::fs::read(&path).expect("WAL after interruption");
2260            if replaced {
2261                let mut expected = reddb_file::encode_wal_file_header().to_vec();
2262                expected.extend_from_slice(&record.encode());
2263                assert_eq!(after, expected);
2264            } else {
2265                assert_eq!(after, original);
2266            }
2267            let coordinator = StoreCommitCoordinator::open(
2268                &path,
2269                DurabilityMode::Strict,
2270                GroupCommitOptions::default(),
2271            )
2272            .expect("reopen");
2273            coordinator.truncate().expect("retry rewrite");
2274            let records: Vec<_> = WalReader::open(&path)
2275                .expect("reader")
2276                .iter()
2277                .map(|entry| entry.expect("record").1)
2278                .collect();
2279            assert_eq!(records, vec![record]);
2280        }
2281    }
2282
2283    #[test]
2284    fn ownership_epoch_rejects_nested_begin_and_wrong_finish() {
2285        let directory = tempfile::tempdir().expect("temporary store");
2286        let store = UnifiedStore::open(directory.path().join("data.rdb")).expect("store");
2287        store.create_collection("rows").expect("collection");
2288        store.persist().expect("baseline");
2289        let coordinator = store.commit.as_ref().expect("WAL coordinator");
2290        assert!(coordinator.begin_page_ownership_epoch(&store, 0).is_err());
2291        assert!(coordinator.finish_page_ownership_epoch(&store, 1).is_err());
2292        coordinator
2293            .begin_page_ownership_epoch(&store, 2)
2294            .expect("begin epoch");
2295        assert!(coordinator.begin_page_ownership_epoch(&store, 3).is_err());
2296        assert!(coordinator.finish_page_ownership_epoch(&store, 3).is_err());
2297        assert!(coordinator.finish_page_ownership_epoch(&store, 2).is_err());
2298        store.flush_paged_registry().expect("publish epoch");
2299        coordinator
2300            .finish_page_ownership_epoch(&store, 2)
2301            .expect("finish epoch");
2302        assert_eq!(store.ownership_active_epoch.load(Ordering::Acquire), 0);
2303    }
2304
2305    #[test]
2306    fn native_pager_records_page_ownership_in_durable_wal() {
2307        use reddb_file::page_ownership_journal::PageOwnershipRecord;
2308
2309        let directory = tempfile::tempdir().expect("temporary store");
2310        let path = directory.path().join("data.rdb");
2311        let wal_path = reddb_file::layout::unified_wal_path(&path);
2312        let pager = crate::storage::engine::Pager::open(&path, Default::default())
2313            .expect("open native pager");
2314        let coordinator = Arc::new(
2315            StoreCommitCoordinator::open(
2316                &wal_path,
2317                DurabilityMode::Async,
2318                GroupCommitOptions::default(),
2319            )
2320            .expect("open WAL"),
2321        );
2322        let recorder = Arc::new(
2323            StoreOwnershipRecorder::begin(Arc::clone(&coordinator), 7).expect("begin epoch"),
2324        );
2325        pager
2326            .install_ownership_recorder(recorder)
2327            .expect("install recorder");
2328        let page = pager
2329            .allocate_page(crate::storage::engine::PageType::BTreeLeaf)
2330            .expect("allocate")
2331            .page_id();
2332        pager
2333            .record_retirement_intent(&[page])
2334            .expect("record retirement");
2335        pager.free_retired_pages(&[page]).expect("retire");
2336        let records: Vec<_> = WalReader::open(&wal_path)
2337            .expect("read WAL")
2338            .iter()
2339            .map(|item| item.expect("WAL record").1)
2340            .collect();
2341        assert_eq!(
2342            records,
2343            vec![
2344                WalRecord::PageOwnership {
2345                    record: PageOwnershipRecord::Begin { epoch: 7 }
2346                },
2347                WalRecord::PageOwnership {
2348                    record: PageOwnershipRecord::Reserve {
2349                        epoch: 7,
2350                        page_id: page
2351                    }
2352                },
2353                WalRecord::PageOwnership {
2354                    record: PageOwnershipRecord::Retire {
2355                        epoch: 7,
2356                        page_id: page
2357                    }
2358                },
2359                WalRecord::PageOwnership {
2360                    record: PageOwnershipRecord::Retire {
2361                        epoch: 7,
2362                        page_id: page
2363                    }
2364                },
2365            ]
2366        );
2367        assert!(
2368            coordinator.fsync_count() >= 3,
2369            "async ownership writes must sync"
2370        );
2371        assert!(
2372            coordinator.truncate().is_err(),
2373            "ownership WAL remains protected"
2374        );
2375    }
2376
2377    #[test]
2378    fn replay_reclaims_unpublished_ownership_reservations() {
2379        use reddb_file::page_ownership_journal::PageOwnershipRecord;
2380
2381        let directory = tempfile::tempdir().expect("temporary store");
2382        let path = directory.path().join("data.rdb");
2383        {
2384            let store = UnifiedStore::open(&path).expect("create store");
2385            store.create_collection("rows").expect("collection");
2386            store.persist().expect("persist baseline");
2387        }
2388        let wal_path = reddb_file::layout::unified_wal_path(&path);
2389        let ids;
2390        {
2391            let pager =
2392                crate::storage::engine::Pager::open(&path, Default::default()).expect("pager");
2393            let first = pager.page_count().expect("page count");
2394            let coordinator = StoreCommitCoordinator::open(
2395                &wal_path,
2396                DurabilityMode::Strict,
2397                GroupCommitOptions::default(),
2398            )
2399            .expect("WAL");
2400            coordinator
2401                .append_durable_page_ownership_batch(&[
2402                    PageOwnershipRecord::Begin { epoch: 2 },
2403                    PageOwnershipRecord::Reserve {
2404                        epoch: 2,
2405                        page_id: first,
2406                    },
2407                    PageOwnershipRecord::Reserve {
2408                        epoch: 2,
2409                        page_id: first + 1,
2410                    },
2411                ])
2412                .expect("reserve pages durably");
2413            ids = [
2414                pager
2415                    .allocate_page(crate::storage::engine::PageType::BTreeLeaf)
2416                    .expect("allocate")
2417                    .page_id(),
2418                pager
2419                    .allocate_page(crate::storage::engine::PageType::BTreeLeaf)
2420                    .expect("allocate")
2421                    .page_id(),
2422            ];
2423            assert_eq!(ids, [first, first + 1]);
2424            pager.sync().expect("persist abandoned pages");
2425        }
2426        let reopened = UnifiedStore::open(&path).expect("recover ownership");
2427        let pager = reopened.pager.as_ref().expect("pager");
2428        let a = pager
2429            .allocate_page(crate::storage::engine::PageType::BTreeLeaf)
2430            .expect("reuse")
2431            .page_id();
2432        let b = pager
2433            .allocate_page(crate::storage::engine::PageType::BTreeLeaf)
2434            .expect("reuse")
2435            .page_id();
2436        assert_ne!(a, b);
2437        assert!(ids.contains(&a) && ids.contains(&b));
2438        let records: Vec<_> = WalReader::open(&wal_path)
2439            .expect("read WAL")
2440            .iter()
2441            .collect::<io::Result<Vec<_>>>()
2442            .expect("records");
2443        assert!(matches!(
2444            records.last(),
2445            Some((
2446                _,
2447                WalRecord::PageOwnership {
2448                    record: PageOwnershipRecord::Finish { epoch: 2 }
2449                }
2450            ))
2451        ));
2452        reopened
2453            .commit
2454            .as_ref()
2455            .expect("coordinator")
2456            .truncate()
2457            .expect("completed ownership can be truncated");
2458    }
2459
2460    #[cfg(target_os = "linux")]
2461    #[test]
2462    fn ownership_recovery_survives_interruption_and_retry() {
2463        use reddb_file::page_ownership_journal::PageOwnershipRecord;
2464
2465        const CHILD: &str = "REDDB_EXPERIMENTAL_OWNERSHIP_RECOVERY_PATH";
2466        if let Some(path) = std::env::var_os(CHILD) {
2467            let _store = UnifiedStore::open(path).expect("recover store");
2468            panic!("recovery interruption hook was not reached");
2469        }
2470        for (published, hook, exit_code) in [
2471            (
2472                false,
2473                "REDDB_EXPERIMENTAL_EXIT_DURING_OWNERSHIP_RECOVERY",
2474                187,
2475            ),
2476            (false, "REDDB_EXPERIMENTAL_EXIT_AFTER_OWNERSHIP_SYNC", 188),
2477            (false, "REDDB_EXPERIMENTAL_EXIT_AFTER_OWNERSHIP_FINISH", 189),
2478            (
2479                true,
2480                "REDDB_EXPERIMENTAL_EXIT_DURING_OWNERSHIP_RECOVERY",
2481                187,
2482            ),
2483            (true, "REDDB_EXPERIMENTAL_EXIT_AFTER_OWNERSHIP_SYNC", 188),
2484            (true, "REDDB_EXPERIMENTAL_EXIT_AFTER_OWNERSHIP_FINISH", 189),
2485        ] {
2486            let directory = tempfile::tempdir().expect("temporary store");
2487            let path = directory.path().join("data.rdb");
2488            {
2489                let store = UnifiedStore::open(&path).expect("create store");
2490                store.create_collection("rows").expect("collection");
2491                store.persist().expect("baseline");
2492                if published {
2493                    store.ownership_active_epoch.store(2, Ordering::Release);
2494                    store.flush_paged_registry().expect("publish epoch");
2495                    store.ownership_active_epoch.store(0, Ordering::Release);
2496                }
2497            }
2498            let wal_path = reddb_file::layout::unified_wal_path(&path);
2499            let ids;
2500            {
2501                let pager =
2502                    crate::storage::engine::Pager::open(&path, Default::default()).expect("pager");
2503                let first = pager.page_count().expect("page count");
2504                let coordinator = StoreCommitCoordinator::open(
2505                    &wal_path,
2506                    DurabilityMode::Strict,
2507                    GroupCommitOptions::default(),
2508                )
2509                .expect("WAL");
2510                let intent = |page_id| {
2511                    if published {
2512                        PageOwnershipRecord::Retire { epoch: 2, page_id }
2513                    } else {
2514                        PageOwnershipRecord::Reserve { epoch: 2, page_id }
2515                    }
2516                };
2517                coordinator
2518                    .append_durable_page_ownership_batch(&[
2519                        PageOwnershipRecord::Begin { epoch: 2 },
2520                        intent(first),
2521                        intent(first + 1),
2522                    ])
2523                    .expect("reserve");
2524                ids = [
2525                    pager
2526                        .allocate_page(crate::storage::engine::PageType::BTreeLeaf)
2527                        .expect("allocate")
2528                        .page_id(),
2529                    pager
2530                        .allocate_page(crate::storage::engine::PageType::BTreeLeaf)
2531                        .expect("allocate")
2532                        .page_id(),
2533                ];
2534                pager.sync().expect("persist abandoned pages");
2535            }
2536            for _ in 0..if exit_code == 189 { 1 } else { 2 } {
2537                let output = std::process::Command::new(std::env::current_exe().expect("test executable"))
2538                    .args(["--exact", "storage::unified::store::commit::tests::ownership_recovery_survives_interruption_and_retry", "--nocapture"])
2539                    .env(CHILD, &path)
2540                    .env(hook, "1")
2541                    .output().expect("run interrupted recovery");
2542                assert_eq!(
2543                    output.status.code(),
2544                    Some(exit_code),
2545                    "published={published} {hook}: {}",
2546                    String::from_utf8_lossy(&output.stderr)
2547                );
2548            }
2549            let reopened = UnifiedStore::open(&path).expect("recover after interruptions");
2550            let pager = reopened.pager.as_ref().expect("pager");
2551            let a = pager
2552                .allocate_page(crate::storage::engine::PageType::BTreeLeaf)
2553                .expect("reuse")
2554                .page_id();
2555            let b = pager
2556                .allocate_page(crate::storage::engine::PageType::BTreeLeaf)
2557                .expect("reuse")
2558                .page_id();
2559            assert_ne!(a, b, "published={published} {hook}: duplicate page");
2560            assert!(
2561                ids.contains(&a) && ids.contains(&b),
2562                "published={published} {hook}: lost recovered page"
2563            );
2564        }
2565    }
2566
2567    #[test]
2568    fn ownership_intents_are_durable_in_every_mode() {
2569        use reddb_file::page_ownership_journal::PageOwnershipRecord;
2570
2571        for mode in [
2572            DurabilityMode::Strict,
2573            DurabilityMode::WalDurableGrouped,
2574            DurabilityMode::Async,
2575        ] {
2576            let directory = tempfile::tempdir().expect("temporary WAL");
2577            let path = directory.path().join("ownership");
2578            let coordinator =
2579                StoreCommitCoordinator::open(&path, mode, GroupCommitOptions::default())
2580                    .expect("open coordinator");
2581            let intent = PageOwnershipRecord::Reserve {
2582                epoch: 1,
2583                page_id: 67,
2584            };
2585            let detach = PageOwnershipRecord::DetachFree {
2586                epoch: 1,
2587                page_id: 68,
2588            };
2589            coordinator
2590                .append_durable_page_ownership_batch(&[intent, detach])
2591                .expect("durable intent");
2592            assert!(
2593                coordinator.fsync_count() > 0,
2594                "ownership intent must be synced in {mode:?}"
2595            );
2596            let records: Vec<_> = WalReader::open(&path)
2597                .expect("open WAL")
2598                .iter()
2599                .collect::<io::Result<Vec<_>>>()
2600                .expect("read records");
2601            assert!(
2602                matches!(records.as_slice(), [(_, WalRecord::PageOwnership { record: first }), (_, WalRecord::PageOwnership { record: second })] if *first == intent && *second == detach)
2603            );
2604            assert!(coordinator
2605                .append_single_record(WalRecord::PageOwnership { record: intent })
2606                .is_err());
2607            assert!(coordinator
2608                .append_durable_page_ownership(PageOwnershipRecord::Begin { epoch: 0 })
2609                .is_err());
2610            assert!(
2611                coordinator.truncate().is_err(),
2612                "active ownership must prevent truncation"
2613            );
2614            let before = std::fs::read(&path).expect("ownership WAL");
2615            drop(coordinator);
2616            let reopened = StoreCommitCoordinator::open(&path, mode, GroupCommitOptions::default())
2617                .expect("reopen coordinator");
2618            assert!(
2619                reopened.truncate().is_err(),
2620                "reopen must rediscover ownership records"
2621            );
2622            assert_eq!(std::fs::read(&path).expect("preserved WAL"), before);
2623        }
2624    }
2625
2626    #[test]
2627    fn unresolved_ownership_intent_blocks_store_replay() {
2628        for truncated in [false, true] {
2629            let directory = tempfile::tempdir().expect("temporary store");
2630            let path = directory.path().join("data.rdb");
2631            {
2632                let store = UnifiedStore::open(&path).expect("create store");
2633                store.create_collection("rows").expect("create collection");
2634                store.persist().expect("persist baseline");
2635            }
2636            let wal_path = reddb_file::layout::unified_wal_path(&path);
2637            {
2638                let mut writer = WalWriter::open(&wal_path).expect("open WAL");
2639                writer
2640                    .append(&WalRecord::PageOwnership {
2641                        record: reddb_file::page_ownership_journal::PageOwnershipRecord::Reserve {
2642                            epoch: 1,
2643                            page_id: 67,
2644                        },
2645                    })
2646                    .expect("append ownership intent");
2647                writer.sync().expect("sync ownership intent");
2648            }
2649            if truncated {
2650                let wal = std::fs::OpenOptions::new()
2651                    .write(true)
2652                    .open(&wal_path)
2653                    .expect("WAL");
2654                let length = wal.metadata().expect("length").len();
2655                wal.set_len(length - 1).expect("truncate last byte");
2656                wal.sync_all().expect("sync truncation");
2657            }
2658            let before = std::fs::read(&path).expect("baseline data");
2659            let error = match UnifiedStore::open(&path) {
2660                Ok(_) => panic!("unresolved ownership must prevent opening"),
2661                Err(error) => error,
2662            };
2663            let expected = if truncated {
2664                "incomplete page ownership WAL record"
2665            } else {
2666                "page ownership journal requires recovery"
2667            };
2668            assert!(error.to_string().contains(expected), "{error}");
2669            assert_eq!(std::fs::read(&path).expect("preserved data"), before);
2670        }
2671    }
2672
2673    #[test]
2674    fn logical_wal_replay_restores_a_deleted_physical_identity() {
2675        for flat in [false, true] {
2676            let store = UnifiedStore::new();
2677            store.create_collection("replay").expect("collection");
2678            let id = EntityId::new(42);
2679            let entity = UnifiedEntity::table_row(id, "replay", 42, vec![Value::Integer(1)]);
2680            if flat {
2681                store
2682                    .bulk_insert("replay", vec![entity])
2683                    .expect("flat seed");
2684            } else {
2685                store.insert_auto("replay", entity).expect("hash seed");
2686            }
2687            let metadata = Metadata::with_fields(HashMap::from([(
2688                "source".into(),
2689                MetadataValue::String("wal".into()),
2690            )]));
2691            for sequence in 100..104 {
2692                let delete = StoreWalAction::DeleteEntityRecord {
2693                    collection: "replay".into(),
2694                    entity_id: id.raw(),
2695                };
2696                store
2697                    .apply_encoded_store_wal_action(&delete.encode())
2698                    .expect("replay delete");
2699                let mut restored =
2700                    UnifiedEntity::table_row(id, "replay", 42, vec![Value::Integer(2)]);
2701                restored.ensure_table_logical_id();
2702                restored.sequence_id = sequence;
2703                let upsert = StoreWalAction::upsert_entity(
2704                    "replay",
2705                    &restored,
2706                    Some(&metadata),
2707                    store.format_version(),
2708                );
2709                store
2710                    .apply_encoded_store_wal_action(&upsert.encode())
2711                    .expect("replay upsert");
2712                let manager = store.get_collection("replay").expect("restored collection");
2713                let stored = manager.get(id).expect("restored entity is readable");
2714                assert_eq!(store.entity_cache_stats().entries, 0);
2715                assert_eq!(manager.count(), 1);
2716                assert_eq!(manager.query_all(|_| true).len(), 1);
2717                assert_eq!(stored.sequence_id, sequence);
2718                assert_eq!(
2719                    UnifiedStore::serialize_entity_record(
2720                        &stored,
2721                        manager.get_metadata(id).as_ref(),
2722                        store.format_version()
2723                    ),
2724                    UnifiedStore::serialize_entity_record(
2725                        &restored,
2726                        Some(&metadata),
2727                        store.format_version()
2728                    )
2729                );
2730                assert!(store.get_any(id).is_some());
2731            }
2732        }
2733    }
2734
2735    fn embedded_fixture(
2736        mode: DurabilityMode,
2737        background_bytes: usize,
2738    ) -> (tempfile::TempDir, PathBuf, UnifiedStore) {
2739        let directory = tempfile::tempdir().expect("directory");
2740        let path = directory.path().join("data.rdb");
2741        let mut store =
2742            UnifiedStore::with_config(UnifiedStoreConfig::default().with_durability_mode(mode));
2743        store.create_collection("keys").expect("collection");
2744        store.set_aux_metadata(vec![42; background_bytes]);
2745        crate::storage::EmbeddedRdbArtifact::create_with_snapshot(
2746            &path,
2747            &store.to_binary_dump_bytes(),
2748        )
2749        .expect("snapshot");
2750        store.config.embedded_wal_path = Some(path.clone());
2751        (directory, path, store)
2752    }
2753
2754    #[test]
2755    fn embedded_stale_full_result_does_not_checkpoint_reclaimed_space() {
2756        for mode in [DurabilityMode::Strict, DurabilityMode::WalDurableGrouped] {
2757            let (_directory, path, store) = embedded_fixture(mode, 0);
2758            store
2759                .append_embedded_store_wal_actions(&[StoreWalAction::CreateCollection {
2760                    name: "keys".into(),
2761                }])
2762                .expect("another writer appended after reclaiming space");
2763            let before = crate::storage::EmbeddedRdbArtifact::open(&path).expect("before");
2764            let payloads = vec![StoreWalAction::CreateCollection {
2765                name: "after".into(),
2766            }
2767            .encode()];
2768            let length = crate::storage::EmbeddedRdbArtifact::wal_payloads_encoded_len(&payloads)
2769                .expect("encoded length");
2770            // Model a full result obtained before that other writer's checkpoint.
2771            store
2772                .recover_full_embedded_wal(&path, &payloads, length)
2773                .expect("revalidate after acquiring publication");
2774            let after = crate::storage::EmbeddedRdbArtifact::open(&path).expect("after");
2775            assert_eq!(
2776                after.selected_superblock.generation,
2777                before.selected_superblock.generation + 1,
2778                "only the retry is published, without an unnecessary checkpoint"
2779            );
2780            assert_eq!(
2781                crate::storage::EmbeddedRdbArtifact::read_wal_payloads(&after)
2782                    .expect("both appends retained")
2783                    .len(),
2784                2
2785            );
2786        }
2787    }
2788
2789    #[test]
2790    fn embedded_full_wal_reserves_bounded_headroom_for_populated_store() {
2791        for mode in [DurabilityMode::Strict, DurabilityMode::WalDurableGrouped] {
2792            let (_directory, path, store) = embedded_fixture(mode, 2 * 1024 * 1024);
2793            let initial = crate::storage::EmbeddedRdbArtifact::open(&path).expect("initial");
2794            let action = StoreWalAction::CreateCollection {
2795                name: "keys".into(),
2796            };
2797            let payloads = vec![action.encode()];
2798            let length = crate::storage::EmbeddedRdbArtifact::wal_payloads_encoded_len(&payloads)
2799                .expect("encoded length");
2800            crate::storage::EmbeddedRdbArtifact::append_wal_payloads(
2801                &path,
2802                &vec![payloads[0].clone(); (initial.manifest.wal_region_bytes / length) as usize],
2803            )
2804            .expect("fill the initial region with valid records");
2805            store
2806                .append_embedded_store_wal_actions(&[action])
2807                .expect("overflow");
2808            let open = crate::storage::EmbeddedRdbArtifact::open(&path).expect("checkpoint");
2809            assert_eq!(open.manifest.wal_region_bytes, 1024 * 1024);
2810            let snapshot = crate::storage::EmbeddedRdbArtifact::read_snapshot(&open)
2811                .expect("snapshot")
2812                .expect("populated image");
2813            let recovered =
2814                UnifiedStore::load_from_bytes_with_config(&snapshot, UnifiedStoreConfig::default())
2815                    .expect("recover snapshot");
2816            for payload in
2817                crate::storage::EmbeddedRdbArtifact::read_wal_payloads(&open).expect("WAL")
2818            {
2819                recovered
2820                    .apply_encoded_store_wal_action(&payload)
2821                    .expect("replay");
2822            }
2823            assert_eq!(
2824                recovered.resident.aux_metadata.read().len(),
2825                2 * 1024 * 1024
2826            );
2827            assert!(recovered.get_collection("keys").is_some());
2828        }
2829    }
2830
2831    #[test]
2832    fn embedded_grouped_overwrites_recover_across_repeated_full_wal_regions() {
2833        fn row(writer: usize, round: usize) -> UnifiedEntity {
2834            let mut state = (writer as u64 + 1) * 31 + round as u64 * 13;
2835            let payload: String = (0..4096)
2836                .map(|_| {
2837                    state = state.wrapping_mul(1664525).wrapping_add(1013904223);
2838                    char::from(b'!' + ((state >> 24) % 90) as u8)
2839                })
2840                .collect();
2841            let mut entity = UnifiedEntity::table_row(EntityId::new(0), "keys", 0, vec![]);
2842            entity.data = EntityData::Row(RowData::with_names(
2843                vec![
2844                    Value::text(format!("k{writer}")),
2845                    Value::text(format!("{round}:{payload}")),
2846                ],
2847                vec!["key".into(), "value".into()],
2848            ));
2849            entity
2850        }
2851        let (_directory, path, store) =
2852            embedded_fixture(DurabilityMode::WalDurableGrouped, 2 * 1024 * 1024);
2853        assert!(
2854            StoreWalAction::upsert_entity("keys", &row(0, 0), None, store.format_version())
2855                .encode()
2856                .len()
2857                >= 2048,
2858            "fixture must exert physical WAL pressure after value compression"
2859        );
2860        let mut ids: Vec<_> = (0..8)
2861            .map(|writer| {
2862                store
2863                    .write_keyed_row("keys", row(writer, 0), None, None)
2864                    .expect("seed")
2865            })
2866            .collect();
2867        let initial = crate::storage::EmbeddedRdbArtifact::open(&path).expect("initial roots");
2868        let mut previous_snapshot = crate::storage::EmbeddedRdbArtifact::read_snapshot(&initial)
2869            .expect("initial snapshot")
2870            .expect("image");
2871        // Each wave writes over 2 MiB through the same 1 MiB reservation.
2872        // Checking image changes after each wave prevents a larger WAL from
2873        // merely postponing every checkpoint beyond the test's duration.
2874        for wave in 0..3 {
2875            let barrier = Barrier::new(ids.len());
2876            ids = std::thread::scope(|scope| {
2877                let workers: Vec<_> = ids
2878                    .iter()
2879                    .enumerate()
2880                    .map(|(writer, previous)| {
2881                        let store = &store;
2882                        let barrier = &barrier;
2883                        scope.spawn(move || {
2884                            barrier.wait();
2885                            let mut id = *previous;
2886                            for round in 0..64 {
2887                                id = store
2888                                    .write_keyed_row(
2889                                        "keys",
2890                                        row(writer, wave * 64 + round),
2891                                        None,
2892                                        Some(id),
2893                                    )
2894                                    .expect("durable overwrite");
2895                            }
2896                            id
2897                        })
2898                    })
2899                    .collect();
2900                workers
2901                    .into_iter()
2902                    .map(|worker| worker.join().expect("writer"))
2903                    .collect()
2904            });
2905            let open = crate::storage::EmbeddedRdbArtifact::open(&path).expect("wave roots");
2906            assert_eq!(open.manifest.wal_region_bytes, EMBEDDED_WAL_HEADROOM_LIMIT);
2907            let snapshot = crate::storage::EmbeddedRdbArtifact::read_snapshot(&open)
2908                .expect("wave snapshot")
2909                .expect("image");
2910            assert!(snapshot != previous_snapshot, "wave {wave} must checkpoint");
2911            previous_snapshot = snapshot;
2912        }
2913        let open = crate::storage::EmbeddedRdbArtifact::open(&path).expect("recover roots");
2914        let snapshot = crate::storage::EmbeddedRdbArtifact::read_snapshot(&open)
2915            .expect("snapshot")
2916            .expect("image");
2917        let recovered =
2918            UnifiedStore::load_from_bytes_with_config(&snapshot, UnifiedStoreConfig::default())
2919                .expect("snapshot replay");
2920        for payload in crate::storage::EmbeddedRdbArtifact::read_wal_payloads(&open).expect("WAL") {
2921            recovered
2922                .apply_encoded_store_wal_action(&payload)
2923                .expect("WAL replay");
2924        }
2925        assert_eq!(recovered.get_collection("keys").expect("keys").count(), 8);
2926        for (writer, id) in ids.iter().enumerate() {
2927            let entity = recovered
2928                .get("keys", *id)
2929                .expect("last acknowledged version");
2930            assert_eq!(
2931                entity.data.as_row().expect("row").get_field("value"),
2932                row(writer, 191)
2933                    .data
2934                    .as_row()
2935                    .expect("expected row")
2936                    .get_field("value")
2937            );
2938        }
2939    }
2940
2941    /// Serialise tests that mutate `REDDB_GROUP_COMMIT_WINDOW_US`.
2942    /// The env table is process-global, so two parallel test threads
2943    /// flipping it would race the assertions in the test that
2944    /// happens to read it last.
2945    fn env_lock() -> &'static StdMutex<()> {
2946        static LOCK: OnceLock<StdMutex<()>> = OnceLock::new();
2947        LOCK.get_or_init(|| StdMutex::new(()))
2948    }
2949
2950    fn temp_wal(name: &str) -> PathBuf {
2951        let nanos = SystemTime::now()
2952            .duration_since(std::time::UNIX_EPOCH)
2953            .unwrap()
2954            .as_nanos();
2955        let path = reddb_file::layout::store_commit_coord_temp_wal_path(
2956            &std::env::temp_dir(),
2957            name,
2958            std::process::id(),
2959            nanos,
2960        );
2961        let _ = std::fs::remove_file(&path);
2962        path
2963    }
2964
2965    fn replay_crash_payload(id: u64, round: u64) -> String {
2966        let mut state = id * 31 + round * 13 + 1;
2967        (0..600)
2968            .map(|_| {
2969                state = state.wrapping_mul(1664525).wrapping_add(1013904223);
2970                char::from(b'!' + ((state >> 24) % 90) as u8)
2971            })
2972            .collect()
2973    }
2974
2975    #[cfg(target_os = "linux")]
2976    #[test]
2977    fn interrupted_replay_reclaims_unpublished_pages() {
2978        interrupted_replay_reclaims_pages(false, false);
2979    }
2980
2981    #[cfg(target_os = "linux")]
2982    #[test]
2983    fn interrupted_replay_reclaims_durable_unpublished_pages() {
2984        interrupted_replay_reclaims_pages(false, true);
2985    }
2986
2987    #[cfg(target_os = "linux")]
2988    #[test]
2989    fn interrupted_replay_preserves_published_pages() {
2990        interrupted_replay_reclaims_pages(true, false);
2991    }
2992
2993    #[cfg(target_os = "linux")]
2994    #[test]
2995    fn interrupted_checkpoint_reclaims_unpublished_pages() {
2996        const CHILD_PATH: &str = "REDDB_EXPERIMENTAL_CHECKPOINT_REBUILD_PATH";
2997        let config = || {
2998            UnifiedStoreConfig::default()
2999                .with_durability_mode(DurabilityMode::Strict)
3000                .with_page_cache_slots(4)
3001        };
3002        if let Some(path) = std::env::var_os(CHILD_PATH) {
3003            let store = UnifiedStore::open_with_config(path, config()).unwrap();
3004            // Inject resident changes without a WAL record: after the aborted
3005            // checkpoint the old durable rows must remain authoritative.
3006            for id in 1..=200 {
3007                let entity = UnifiedEntity::table_row(
3008                    EntityId::new(id),
3009                    "rows",
3010                    id,
3011                    vec![Value::text(replay_crash_payload(id, 8))],
3012                );
3013                let record =
3014                    UnifiedStore::serialize_entity_record(&entity, None, STORE_VERSION_V11);
3015                store.apply_replayed_upsert("rows", &record, false).unwrap();
3016            }
3017            store.persist().expect("must exit during checkpoint");
3018            panic!("checkpoint rebuild hook was not reached");
3019        }
3020        let directory = tempfile::tempdir().unwrap();
3021        let path = directory.path().join("data.rdb");
3022        {
3023            let store = UnifiedStore::open_with_config(&path, config()).unwrap();
3024            store.create_collection("rows").unwrap();
3025            for id in 1..=200 {
3026                store
3027                    .insert(
3028                        "rows",
3029                        UnifiedEntity::table_row(
3030                            EntityId::new(id),
3031                            "rows",
3032                            id,
3033                            vec![Value::text(format!("initial-{id}"))],
3034                        ),
3035                    )
3036                    .unwrap();
3037            }
3038            store.persist().unwrap();
3039        }
3040        let mut first = 0;
3041        let mut last = 0;
3042        for cycle in 1..=12 {
3043            let output = std::process::Command::new(std::env::current_exe().unwrap())
3044                .args([
3045                    "--exact",
3046                    "storage::unified::store::commit::tests::interrupted_checkpoint_reclaims_unpublished_pages",
3047                    "--nocapture",
3048                ])
3049                .env(CHILD_PATH, &path)
3050                .env("REDDB_EXPERIMENTAL_EXIT_DURING_CHECKPOINT_REBUILD", "1")
3051                .output()
3052                .unwrap();
3053            assert_eq!(
3054                output.status.code(),
3055                Some(183),
3056                "{}",
3057                String::from_utf8_lossy(&output.stderr)
3058            );
3059            assert!(String::from_utf8_lossy(&output.stderr)
3060                .contains("experimental checkpoint exit before manifest"));
3061            last = std::fs::metadata(&path).unwrap().len();
3062            if cycle == 1 {
3063                first = last;
3064            }
3065            eprintln!("checkpoint_exit_cycle={cycle} data_logical={last}");
3066        }
3067        let recovered = UnifiedStore::open_with_config(&path, config()).unwrap();
3068        assert_eq!(recovered.get_collection("rows").unwrap().count(), 200);
3069        for id in 1..=200 {
3070            assert_eq!(
3071                recovered
3072                    .get("rows", EntityId::new(id))
3073                    .unwrap()
3074                    .data
3075                    .as_row()
3076                    .unwrap()
3077                    .columns,
3078                vec![Value::text(format!("initial-{id}"))]
3079            );
3080        }
3081        assert!(last <= first + 2 * crate::storage::engine::PAGE_SIZE as u64,
3082            "repeated interrupted checkpoint must bound page growth: first={first}, after_12={last}");
3083    }
3084
3085    #[cfg(target_os = "linux")]
3086    #[test]
3087    fn interrupted_checkpoint_with_wal_reclaims_unpublished_pages() {
3088        use std::os::unix::fs::MetadataExt;
3089        const CHILD_PATH: &str = "REDDB_EXPERIMENTAL_CHECKPOINT_WAL_PATH";
3090        const ROUND: &str = "REDDB_EXPERIMENTAL_CHECKPOINT_WAL_ROUND";
3091        let config = || {
3092            UnifiedStoreConfig::default()
3093                .with_durability_mode(DurabilityMode::Strict)
3094                .with_page_cache_slots(4)
3095        };
3096        if let Some(path) = std::env::var_os(CHILD_PATH) {
3097            let round: u64 = std::env::var(ROUND).unwrap().parse().unwrap();
3098            let store = UnifiedStore::open_with_config(path, config()).unwrap();
3099            if round > 1 {
3100                for id in 1..=200 {
3101                    assert_eq!(
3102                        store
3103                            .get("rows", EntityId::new(id))
3104                            .unwrap()
3105                            .data
3106                            .as_row()
3107                            .unwrap()
3108                            .columns,
3109                        vec![Value::text(replay_crash_payload(id, round - 1))]
3110                    );
3111                }
3112            }
3113            let actions: Vec<_> = (1..=200)
3114                .map(|id| {
3115                    let entity = UnifiedEntity::table_row(
3116                        EntityId::new(id),
3117                        "rows",
3118                        id,
3119                        vec![Value::text(replay_crash_payload(id, round))],
3120                    );
3121                    StoreWalAction::upsert_entity("rows", &entity, None, STORE_VERSION_V11)
3122                })
3123                .collect();
3124            for action in &actions {
3125                store.apply_replayed_action(action, false).unwrap();
3126            }
3127            // Use the actual strict WAL coordinator. No checkpoint proof is
3128            // installed, so the replacement-tree path must still be taken.
3129            store
3130                .commit
3131                .as_ref()
3132                .unwrap()
3133                .append_actions(&actions)
3134                .unwrap();
3135            store.persist().expect("must exit during checkpoint");
3136            panic!("checkpoint rebuild hook was not reached");
3137        }
3138        let directory = tempfile::tempdir().unwrap();
3139        let path = directory.path().join("data.rdb");
3140        {
3141            let store = UnifiedStore::open_with_config(&path, config()).unwrap();
3142            store.create_collection("rows").unwrap();
3143            for id in 1..=200 {
3144                store
3145                    .insert(
3146                        "rows",
3147                        UnifiedEntity::table_row(
3148                            EntityId::new(id),
3149                            "rows",
3150                            id,
3151                            vec![Value::text(format!("initial-{id}"))],
3152                        ),
3153                    )
3154                    .unwrap();
3155            }
3156            store.persist().unwrap();
3157        }
3158        let mut fourth = 0;
3159        let mut last = 0;
3160        for round in 1..=12 {
3161            let output = std::process::Command::new(std::env::current_exe().unwrap())
3162                .args([
3163                    "--exact",
3164                    "storage::unified::store::commit::tests::interrupted_checkpoint_with_wal_reclaims_unpublished_pages",
3165                    "--nocapture",
3166                ])
3167                .env(CHILD_PATH, &path)
3168                .env(ROUND, round.to_string())
3169                .env("REDDB_EXPERIMENTAL_EXIT_DURING_CHECKPOINT_REBUILD", "1")
3170                .output().unwrap();
3171            assert_eq!(
3172                output.status.code(),
3173                Some(183),
3174                "round {round}: {}",
3175                String::from_utf8_lossy(&output.stderr)
3176            );
3177            assert!(String::from_utf8_lossy(&output.stderr)
3178                .contains("experimental checkpoint exit before manifest"));
3179            let meta = std::fs::metadata(&path).unwrap();
3180            last = meta.len();
3181            if round == 4 {
3182                fourth = last;
3183            }
3184            eprintln!(
3185                "checkpoint_wal_exit_round={round} data_logical={last} data_allocated={}",
3186                meta.blocks() * 512
3187            );
3188        }
3189        let recovered = UnifiedStore::open_with_config(&path, config()).unwrap();
3190        assert_eq!(recovered.get_collection("rows").unwrap().count(), 200);
3191        for id in 1..=200 {
3192            assert_eq!(
3193                recovered
3194                    .get("rows", EntityId::new(id))
3195                    .unwrap()
3196                    .data
3197                    .as_row()
3198                    .unwrap()
3199                    .columns,
3200                vec![Value::text(replay_crash_payload(id, 12))]
3201            );
3202        }
3203        recovered.persist().unwrap();
3204        drop(recovered);
3205        let reopened = UnifiedStore::open_with_config(&path, config()).unwrap();
3206        for id in 1..=200 {
3207            assert_eq!(
3208                reopened
3209                    .get("rows", EntityId::new(id))
3210                    .unwrap()
3211                    .data
3212                    .as_row()
3213                    .unwrap()
3214                    .columns,
3215                vec![Value::text(replay_crash_payload(id, 12))]
3216            );
3217        }
3218        assert!(last <= fourth + 2 * crate::storage::engine::PAGE_SIZE as u64,
3219            "repeated interrupted WAL-backed checkpoints must bound page growth: round4={fourth}, round12={last}");
3220    }
3221
3222    #[cfg(target_os = "linux")]
3223    #[test]
3224    fn interrupted_retired_reclamation_preserves_reuse() {
3225        const CHILD_PATH: &str = "REDDB_EXPERIMENTAL_RETIRED_PATH";
3226        fn mutate(store: &UnifiedStore, round: u64) {
3227            for id in 1..=200 {
3228                let entity = UnifiedEntity::table_row(
3229                    EntityId::new(id),
3230                    "rows",
3231                    id,
3232                    vec![Value::text(replay_crash_payload(id, round))],
3233                );
3234                let record =
3235                    UnifiedStore::serialize_entity_record(&entity, None, STORE_VERSION_V11);
3236                store.apply_replayed_upsert("rows", &record, false).unwrap();
3237            }
3238        }
3239        let config = || {
3240            UnifiedStoreConfig::default()
3241                .with_durability_mode(DurabilityMode::Strict)
3242                .with_page_cache_slots(4)
3243        };
3244        if let Some(path) = std::env::var_os(CHILD_PATH) {
3245            let store = UnifiedStore::open_with_config(path, config()).unwrap();
3246            mutate(&store, 2);
3247            store.persist().expect("must exit at retired free boundary");
3248            panic!("retired free hook was not reached");
3249        }
3250        let mut reuse = Vec::new();
3251        for (label, env_name, exit_code, message) in [
3252            (
3253                "before",
3254                "REDDB_EXPERIMENTAL_EXIT_BEFORE_RETIRED_FREE",
3255                184,
3256                "experimental exit before retired free",
3257            ),
3258            (
3259                "enqueued",
3260                "REDDB_EXPERIMENTAL_EXIT_AFTER_RETIRED_ENQUEUE",
3261                186,
3262                "experimental exit after retired enqueue",
3263            ),
3264            (
3265                "after",
3266                "REDDB_EXPERIMENTAL_EXIT_AFTER_RETIRED_FREE",
3267                185,
3268                "experimental exit after retired free",
3269            ),
3270        ] {
3271            let directory = tempfile::tempdir().unwrap();
3272            let path = directory.path().join("data.rdb");
3273            {
3274                let store = UnifiedStore::open_with_config(&path, config()).unwrap();
3275                store.create_collection("rows").unwrap();
3276                for id in 1..=200 {
3277                    store
3278                        .insert(
3279                            "rows",
3280                            UnifiedEntity::table_row(
3281                                EntityId::new(id),
3282                                "rows",
3283                                id,
3284                                vec![Value::text(format!("initial-{id}"))],
3285                            ),
3286                        )
3287                        .unwrap();
3288                }
3289                store.persist().unwrap();
3290                mutate(&store, 1);
3291                store.persist().unwrap();
3292            }
3293            let output = std::process::Command::new(std::env::current_exe().unwrap())
3294                .args([
3295                    "--exact",
3296                    "storage::unified::store::commit::tests::interrupted_retired_reclamation_preserves_reuse",
3297                    "--nocapture",
3298                ])
3299                .env(CHILD_PATH, &path)
3300                .env(env_name, "1")
3301                .output().unwrap();
3302            assert_eq!(
3303                output.status.code(),
3304                Some(exit_code),
3305                "{label}: {}",
3306                String::from_utf8_lossy(&output.stderr)
3307            );
3308            assert!(String::from_utf8_lossy(&output.stderr).contains(message));
3309            eprintln!(
3310                "retired_reclaim_{label} child={}",
3311                String::from_utf8_lossy(&output.stderr).trim()
3312            );
3313            let store = UnifiedStore::open_with_config(&path, config()).unwrap();
3314            for id in 1..=200 {
3315                assert_eq!(
3316                    store
3317                        .get("rows", EntityId::new(id))
3318                        .unwrap()
3319                        .data
3320                        .as_row()
3321                        .unwrap()
3322                        .columns,
3323                    vec![Value::text(replay_crash_payload(id, 2))]
3324                );
3325            }
3326            let pager = store.pager.as_ref().unwrap();
3327            let before_pages = pager.page_count().unwrap();
3328            let mut ids = std::collections::HashSet::new();
3329            let mut reused = 0;
3330            for _ in 0..256 {
3331                let page = pager
3332                    .allocate_page(crate::storage::engine::page::PageType::BTreeLeaf)
3333                    .unwrap();
3334                let id = page.page_id();
3335                assert!(ids.insert(id), "duplicate allocation");
3336                reused += u32::from(id < before_pages);
3337                pager.write_page(id, page).unwrap();
3338            }
3339            pager.sync().unwrap();
3340            eprintln!(
3341                "retired_reclaim_{label} before_pages={before_pages} reused={reused} appended={}",
3342                256 - reused
3343            );
3344            drop(store);
3345            let reopened = UnifiedStore::open_with_config(&path, config()).unwrap();
3346            for id in 1..=200 {
3347                assert_eq!(
3348                    reopened
3349                        .get("rows", EntityId::new(id))
3350                        .unwrap()
3351                        .data
3352                        .as_row()
3353                        .unwrap()
3354                        .columns,
3355                    vec![Value::text(replay_crash_payload(id, 2))]
3356                );
3357            }
3358            reuse.push(reused);
3359        }
3360        assert!(
3361            reuse[0] >= reuse[2] && reuse[1] >= reuse[2],
3362            "recovery must not lose retired pages before allocator publication: before={} enqueued={} after={}",
3363            reuse[0],
3364            reuse[1],
3365            reuse[2]
3366        );
3367    }
3368
3369    #[cfg(target_os = "linux")]
3370    #[test]
3371    fn experimental_retaining_released_root_is_not_recoverable() {
3372        let directory = tempfile::tempdir().unwrap();
3373        let path = directory.path().join("data.rdb");
3374        let config = || {
3375            UnifiedStoreConfig::default()
3376                .with_durability_mode(DurabilityMode::Strict)
3377                .with_page_cache_slots(4)
3378        };
3379        {
3380            let store = UnifiedStore::open_with_config(&path, config()).unwrap();
3381            store.create_collection("rows").unwrap();
3382            for id in 1..=200 {
3383                store
3384                    .insert(
3385                        "rows",
3386                        UnifiedEntity::table_row(
3387                            EntityId::new(id),
3388                            "rows",
3389                            id,
3390                            vec![Value::text(format!("initial-{id}"))],
3391                        ),
3392                    )
3393                    .unwrap();
3394            }
3395            store.persist().unwrap();
3396            for id in 1..=200 {
3397                let entity = UnifiedEntity::table_row(
3398                    EntityId::new(id),
3399                    "rows",
3400                    id,
3401                    vec![Value::text(replay_crash_payload(id, 1))],
3402                );
3403                let record =
3404                    UnifiedStore::serialize_entity_record(&entity, None, STORE_VERSION_V11);
3405                store.apply_replayed_upsert("rows", &record, false).unwrap();
3406            }
3407            store.persist().unwrap();
3408        }
3409        let output = std::process::Command::new(std::env::current_exe().unwrap())
3410            .args(["--exact", "storage::unified::store::commit::tests::interrupted_retired_reclamation_preserves_reuse", "--nocapture"])
3411            .env("REDDB_EXPERIMENTAL_RETIRED_PATH", &path)
3412            .env("REDDB_EXPERIMENTAL_RETAIN_RELEASED_ROOT", "1")
3413            .env("REDDB_EXPERIMENTAL_EXIT_AFTER_RETIRED_FREE", "1")
3414            .output().unwrap();
3415        assert_eq!(
3416            output.status.code(),
3417            Some(185),
3418            "{}",
3419            String::from_utf8_lossy(&output.stderr)
3420        );
3421        assert!(String::from_utf8_lossy(&output.stderr)
3422            .contains("experimental exit after retired free"));
3423
3424        let reopened = UnifiedStore::open_with_config(&path, config()).unwrap();
3425        for id in 1..=200 {
3426            assert_eq!(
3427                reopened
3428                    .get("rows", EntityId::new(id))
3429                    .unwrap()
3430                    .data
3431                    .as_row()
3432                    .unwrap()
3433                    .columns,
3434                vec![Value::text(replay_crash_payload(id, 2))]
3435            );
3436        }
3437        let retired_roots: Vec<_> = reopened
3438            .retired_btrees
3439            .lock()
3440            .iter()
3441            .map(|entry| entry.tree.root_page_id())
3442            .collect();
3443        let pager = reopened.pager.as_ref().unwrap();
3444        let root_types: Vec<_> = retired_roots
3445            .iter()
3446            .map(|id| pager.read_page(*id).unwrap().page_type().unwrap())
3447            .collect();
3448        assert!(
3449            root_types.contains(&crate::storage::engine::page::PageType::FreelistTrunk),
3450            "expected a retained root to have become a freelist trunk: {root_types:?}"
3451        );
3452        let error = reopened
3453            .persist()
3454            .expect_err("retained root was reused as allocator metadata");
3455        assert!(
3456            error
3457                .to_string()
3458                .contains("unexpected retired tree page type"),
3459            "{error}"
3460        );
3461        eprintln!("retained_root_types={root_types:?} persist_error={error}");
3462    }
3463
3464    #[cfg(target_os = "linux")]
3465    fn interrupted_replay_reclaims_pages(after_publication: bool, force_sync: bool) {
3466        use std::os::unix::fs::MetadataExt;
3467        const CHILD_PATH: &str = "REDDB_EXPERIMENTAL_REPLAY_REBUILD_PATH";
3468        let config = || {
3469            UnifiedStoreConfig::default()
3470                .with_durability_mode(DurabilityMode::Strict)
3471                .with_page_cache_slots(4)
3472        };
3473        if let Some(path) = std::env::var_os(CHILD_PATH) {
3474            let _store = UnifiedStore::open_with_config(path, config())
3475                .expect("must exit while replay rebuilds");
3476            panic!("replay rebuild hook was not reached");
3477        }
3478        let directory = tempfile::tempdir().unwrap();
3479        let path = directory.path().join("data.rdb");
3480        {
3481            let store = UnifiedStore::open_with_config(&path, config()).unwrap();
3482            store.create_collection("rows").unwrap();
3483            for id in 1..=200 {
3484                store
3485                    .insert(
3486                        "rows",
3487                        UnifiedEntity::table_row(
3488                            EntityId::new(id),
3489                            "rows",
3490                            id,
3491                            vec![Value::text(format!("initial-{id}"))],
3492                        ),
3493                    )
3494                    .unwrap();
3495            }
3496            store.persist().unwrap();
3497        }
3498        {
3499            let coordinator = StoreCommitCoordinator::open(
3500                reddb_file::layout::unified_wal_path(&path),
3501                DurabilityMode::Strict,
3502                GroupCommitOptions::default(),
3503            )
3504            .unwrap();
3505            let actions = (1..=200)
3506                .map(|id| {
3507                    let entity = UnifiedEntity::table_row(
3508                        EntityId::new(id),
3509                        "rows",
3510                        id,
3511                        vec![Value::text(replay_crash_payload(id, 8))],
3512                    );
3513                    StoreWalAction::upsert_entity("rows", &entity, None, STORE_VERSION_V11)
3514                })
3515                .collect::<Vec<_>>();
3516            coordinator.append_actions(&actions).unwrap();
3517        }
3518        let snapshot = || {
3519            let data = std::fs::metadata(&path).unwrap();
3520            let wal = std::fs::metadata(reddb_file::layout::unified_wal_path(&path)).unwrap();
3521            (
3522                data.len(),
3523                data.blocks() * 512,
3524                wal.len(),
3525                wal.blocks() * 512,
3526            )
3527        };
3528        let baseline = snapshot();
3529        let mut previous = baseline;
3530        let mut first_failure = baseline;
3531        for cycle in 1..=12 {
3532            let test_name = if after_publication {
3533                "storage::unified::store::commit::tests::interrupted_replay_preserves_published_pages"
3534            } else if force_sync {
3535                "storage::unified::store::commit::tests::interrupted_replay_reclaims_durable_unpublished_pages"
3536            } else {
3537                "storage::unified::store::commit::tests::interrupted_replay_reclaims_unpublished_pages"
3538            };
3539            let mut command = std::process::Command::new(std::env::current_exe().unwrap());
3540            command
3541                .args(["--exact", test_name, "--nocapture"])
3542                .env(CHILD_PATH, &path);
3543            if !force_sync {
3544                command.env("REDDB_EXPERIMENTAL_REPLAY_NO_SYNC", "1");
3545            } else {
3546                command.env_remove("REDDB_EXPERIMENTAL_REPLAY_NO_SYNC");
3547            }
3548            let message = if after_publication {
3549                command.env("REDDB_EXPERIMENTAL_EXIT_AFTER_REPLAY_PUBLICATION", "1");
3550                "experimental replay exit after manifest"
3551            } else {
3552                command.env("REDDB_EXPERIMENTAL_EXIT_DURING_REPLAY_REBUILD", "1");
3553                "experimental replay rebuild exit before manifest"
3554            };
3555            let output = command.output().unwrap();
3556            assert_eq!(
3557                output.status.code(),
3558                Some(182),
3559                "cycle {cycle}: {}",
3560                String::from_utf8_lossy(&output.stderr)
3561            );
3562            assert!(String::from_utf8_lossy(&output.stderr).contains(message));
3563            let now = snapshot();
3564            eprintln!("replay_exit_cycle={cycle} data_logical={} data_allocated={} wal_logical={} wal_allocated={}",now.0,now.1,now.2,now.3);
3565            if cycle == 1 {
3566                first_failure = now;
3567            }
3568            previous = now;
3569        }
3570        let recovered = UnifiedStore::open_with_config(&path, config()).unwrap();
3571        assert_eq!(recovered.get_collection("rows").unwrap().count(), 200);
3572        for id in 1..=200 {
3573            assert_eq!(
3574                recovered
3575                    .get("rows", EntityId::new(id))
3576                    .unwrap()
3577                    .data
3578                    .as_row()
3579                    .unwrap()
3580                    .columns,
3581                vec![Value::text(replay_crash_payload(id, 8))]
3582            );
3583        }
3584        let pages = recovered.pager.as_ref().unwrap().page_count().unwrap();
3585        let free = recovered.pager.as_ref().unwrap().free_page_count().unwrap();
3586        recovered.persist().unwrap();
3587        drop(recovered);
3588        let final_sizes = snapshot();
3589        let reopened = UnifiedStore::open_with_config(&path, config()).unwrap();
3590        assert_eq!(reopened.get_collection("rows").unwrap().count(), 200);
3591        for id in 1..=200 {
3592            assert_eq!(
3593                reopened
3594                    .get("rows", EntityId::new(id))
3595                    .unwrap()
3596                    .data
3597                    .as_row()
3598                    .unwrap()
3599                    .columns,
3600                vec![Value::text(replay_crash_payload(id, 8))]
3601            );
3602        }
3603        eprintln!("replay_rebuild baseline={baseline:?} after_12={previous:?} after_recovery_checkpoint={final_sizes:?} pages={pages} free={free}");
3604        // Probe actual allocation rather than the cached free-page counter:
3605        // newly allocated IDs reveal whether interrupted generations returned
3606        // to the allocator after recovery and a completed checkpoint.
3607        let pager = reopened.pager.as_ref().unwrap();
3608        let before_probe = pager.page_count().unwrap();
3609        let mut ids = std::collections::HashSet::new();
3610        let mut reused = 0;
3611        for _ in 0..256 {
3612            let page = pager
3613                .allocate_page(crate::storage::engine::page::PageType::BTreeLeaf)
3614                .unwrap();
3615            let id = page.page_id();
3616            assert!(ids.insert(id), "allocator returned the same page twice");
3617            reused += u32::from(id < before_probe);
3618            pager.write_page(id, page).unwrap();
3619        }
3620        pager.sync().unwrap();
3621        let after_probe = pager.page_count().unwrap();
3622        eprintln!("replay_reuse_probe count=256 before_pages={before_probe} after_pages={after_probe} reused={reused} appended={}", 256 - reused);
3623        drop(reopened);
3624        let verified = UnifiedStore::open_with_config(&path, config()).unwrap();
3625        for id in 1..=200 {
3626            assert_eq!(
3627                verified
3628                    .get("rows", EntityId::new(id))
3629                    .unwrap()
3630                    .data
3631                    .as_row()
3632                    .unwrap()
3633                    .columns,
3634                vec![Value::text(replay_crash_payload(id, 8))]
3635            );
3636        }
3637        assert!(
3638            previous.0 <= first_failure.0 + 2 * crate::storage::engine::PAGE_SIZE as u64,
3639            "repeated interrupted replay must bound page growth: first={}, after_12={}",
3640            first_failure.0,
3641            previous.0
3642        );
3643    }
3644
3645    #[test]
3646    fn paged_wal_replay_survives_immediate_process_exit() {
3647        const CHILD_PATH: &str = "REDDB_TEST_REPLAY_CRASH_PATH";
3648        let config = || UnifiedStoreConfig::default().with_page_cache_slots(4);
3649        if let Some(path) = std::env::var_os(CHILD_PATH) {
3650            let store = UnifiedStore::open_with_config(path, config()).expect("replay child");
3651            assert_eq!(store.get_collection("rows").unwrap().count(), 400);
3652            // No Rust destructors or graceful checkpoint may repair the image.
3653            std::process::exit(0);
3654        }
3655        let directory = tempfile::tempdir().unwrap();
3656        let path = directory.path().join("data.rdb");
3657        {
3658            let store = UnifiedStore::open_with_config(&path, config()).unwrap();
3659            store.create_collection("rows").unwrap();
3660            for id in 1..=400 {
3661                store
3662                    .insert(
3663                        "rows",
3664                        UnifiedEntity::table_row(
3665                            EntityId::new(id),
3666                            "rows",
3667                            id,
3668                            vec![Value::text(format!("initial-{id}"))],
3669                        ),
3670                    )
3671                    .unwrap();
3672            }
3673            store.persist().unwrap();
3674        }
3675        {
3676            let coordinator = StoreCommitCoordinator::open(
3677                reddb_file::layout::unified_wal_path(&path),
3678                DurabilityMode::Strict,
3679                GroupCommitOptions::default(),
3680            )
3681            .unwrap();
3682            for round in 0..3 {
3683                let actions = (1..=400)
3684                    .map(|id| {
3685                        let payload = replay_crash_payload(id, round);
3686                        let entity = UnifiedEntity::table_row(
3687                            EntityId::new(id),
3688                            "rows",
3689                            id,
3690                            vec![Value::text(payload)],
3691                        );
3692                        StoreWalAction::upsert_entity("rows", &entity, None, STORE_VERSION_V11)
3693                    })
3694                    .collect::<Vec<_>>();
3695                coordinator.append_actions(&actions).unwrap();
3696            }
3697        }
3698        let status = std::process::Command::new(std::env::current_exe().unwrap())
3699            .args(["--exact", "storage::unified::store::commit::tests::paged_wal_replay_survives_immediate_process_exit", "--nocapture"])
3700            .env(CHILD_PATH, &path).status().unwrap();
3701        assert!(status.success(), "replay process failed");
3702        let reopened = UnifiedStore::open_with_config(&path, config())
3703            .expect("cold reopen after replay crash");
3704        assert_eq!(reopened.get_collection("rows").unwrap().count(), 400);
3705        for id in 1..=400 {
3706            let entity = reopened
3707                .get("rows", EntityId::new(id))
3708                .expect("retained entity");
3709            let EntityData::Row(row) = entity.data else {
3710                panic!("expected row")
3711            };
3712            assert_eq!(row.columns, vec![Value::text(replay_crash_payload(id, 2))]);
3713        }
3714        drop(reopened);
3715        let stable_bytes = std::fs::metadata(&path).unwrap().len();
3716        let repeated = UnifiedStore::open_with_config(&path, config()).unwrap();
3717        assert_eq!(repeated.get_collection("rows").unwrap().count(), 400);
3718        assert_eq!(
3719            std::fs::metadata(&path).unwrap().len(),
3720            stable_bytes,
3721            "idempotent replay must not allocate another tree generation"
3722        );
3723    }
3724
3725    #[test]
3726    fn checkpoint_reset_waits_for_an_enqueued_commit_acknowledgement() {
3727        let _env = env_lock().lock().unwrap_or_else(|p| p.into_inner());
3728        let path = temp_wal("checkpoint-ack");
3729        let coordinator = Arc::new(
3730            StoreCommitCoordinator::open(
3731                path.clone(),
3732                DurabilityMode::WalDurableGrouped,
3733                GroupCommitOptions::default(),
3734            )
3735            .unwrap(),
3736        );
3737        // Pause the writer after enqueue, before it can register/wait for its
3738        // acknowledgement. The flusher also needs this state lock to drain.
3739        let state = coordinator.state.0.lock();
3740        let (written_tx, written_rx) = std::sync::mpsc::channel();
3741        let writer_coordinator = Arc::clone(&coordinator);
3742        let writer = std::thread::spawn(move || {
3743            let result = writer_coordinator.append_actions(&[StoreWalAction::CreateCollection {
3744                name: "acknowledged".into(),
3745            }]);
3746            written_tx.send(result).unwrap();
3747        });
3748        let deadline = Instant::now() + Duration::from_secs(2);
3749        while !coordinator.queue.has_pending() {
3750            assert!(Instant::now() < deadline, "writer never enqueued");
3751            std::thread::yield_now();
3752        }
3753        let (started_tx, started_rx) = std::sync::mpsc::channel();
3754        let (reset_tx, reset_rx) = std::sync::mpsc::channel();
3755        let reset_coordinator = Arc::clone(&coordinator);
3756        let reset = std::thread::spawn(move || {
3757            started_tx.send(()).unwrap();
3758            reset_tx.send(reset_coordinator.truncate()).unwrap();
3759        });
3760        started_rx.recv_timeout(Duration::from_secs(2)).unwrap();
3761        let deadline = Instant::now() + Duration::from_millis(100);
3762        while coordinator.queue.has_pending() && Instant::now() < deadline {
3763            std::thread::sleep(Duration::from_millis(1));
3764        }
3765        let discarded_before_ack = !coordinator.queue.has_pending();
3766        drop(state);
3767        let acknowledged = written_rx.recv_timeout(Duration::from_secs(2));
3768        if acknowledged.is_err() {
3769            // Release an old implementation's stranded waiter before failing
3770            // the assertion, so the regression itself cannot leak a worker.
3771            let mut state = coordinator.state.0.lock();
3772            state.shutdown = true;
3773            state.last_error = Some("checkpoint acknowledgement regression cleanup".into());
3774            coordinator.state.1.notify_all();
3775        }
3776        writer.join().unwrap();
3777        reset_rx
3778            .recv_timeout(Duration::from_secs(2))
3779            .unwrap()
3780            .unwrap();
3781        reset.join().unwrap();
3782        assert!(
3783            !discarded_before_ack,
3784            "checkpoint discarded an unacknowledged WAL record"
3785        );
3786        assert!(
3787            acknowledged.unwrap().is_ok(),
3788            "durable writer did not finish"
3789        );
3790        drop(coordinator);
3791        let _ = std::fs::remove_file(path);
3792    }
3793
3794    /// Concurrent autocommits MUST coalesce into far fewer fsyncs
3795    /// than the number of callers when the adaptive group-commit
3796    /// window is active. Without the 200µs floor, every caller
3797    /// would race the drain loop and pay an independent fsync.
3798    ///
3799    /// The test fires 32 concurrent `append_actions` calls and asserts
3800    /// the coordinator issued strictly fewer fsyncs than callers.
3801    /// (We don't pin to "one fsync" because timing on loaded CI hosts
3802    /// can split the burst across a couple of drain cycles — the win
3803    /// is the ratio, not the absolute count.)
3804    #[test]
3805    fn group_commit_coalesces_concurrent_autocommits() {
3806        let _env = env_lock().lock().unwrap_or_else(|p| p.into_inner());
3807        // Make sure no stale env var skews this run — the test
3808        // exercises the default 200µs floor.
3809        std::env::remove_var("REDDB_GROUP_COMMIT_WINDOW_US");
3810
3811        let path = temp_wal("coalesce");
3812        let coord = Arc::new(
3813            StoreCommitCoordinator::open(
3814                path.clone(),
3815                DurabilityMode::WalDurableGrouped,
3816                GroupCommitOptions::default(),
3817            )
3818            .expect("open commit coordinator"),
3819        );
3820
3821        const WRITERS: usize = 32;
3822        let barrier = Arc::new(Barrier::new(WRITERS));
3823        let mut handles = Vec::with_capacity(WRITERS);
3824        for tx in 0..WRITERS {
3825            let coord_c = Arc::clone(&coord);
3826            let barrier_c = Arc::clone(&barrier);
3827            handles.push(std::thread::spawn(move || {
3828                // Synchronise the start so every writer races the
3829                // drain loop together — this is the workload shape
3830                // the adaptive window is supposed to coalesce.
3831                barrier_c.wait();
3832                let action = StoreWalAction::CreateCollection {
3833                    name: format!("c{tx}"),
3834                };
3835                coord_c
3836                    .append_actions(std::slice::from_ref(&action))
3837                    .expect("append_actions");
3838            }));
3839        }
3840        for h in handles {
3841            h.join().expect("writer thread");
3842        }
3843
3844        let fsyncs = coord.fsync_count();
3845        assert!(fsyncs > 0, "expected at least one fsync, got {fsyncs}");
3846        assert!(
3847            fsyncs < WRITERS as u64,
3848            "expected fsyncs ({fsyncs}) to be strictly less than \
3849             concurrent writers ({WRITERS}); coalescing failed"
3850        );
3851
3852        drop(coord);
3853        let _ = std::fs::remove_file(&path);
3854    }
3855
3856    /// Sanity check: with the env override forcing a zero window,
3857    /// fsync coalescing degrades — callers race and we approach
3858    /// one fsync per caller. This proves the window is the actual
3859    /// knob doing the work above (i.e. the test isn't passing for
3860    /// some unrelated reason like buffered-IO coalescing).
3861    #[test]
3862    fn zero_window_disables_coalescing_floor() {
3863        let _env = env_lock().lock().unwrap_or_else(|p| p.into_inner());
3864        std::env::set_var("REDDB_GROUP_COMMIT_WINDOW_US", "0");
3865
3866        let path = temp_wal("zero_window");
3867        let coord = Arc::new(
3868            StoreCommitCoordinator::open(
3869                path.clone(),
3870                DurabilityMode::WalDurableGrouped,
3871                GroupCommitOptions::default(),
3872            )
3873            .expect("open commit coordinator"),
3874        );
3875
3876        const WRITERS: usize = 8;
3877        let barrier = Arc::new(Barrier::new(WRITERS));
3878        let mut handles = Vec::with_capacity(WRITERS);
3879        for tx in 0..WRITERS {
3880            let coord_c = Arc::clone(&coord);
3881            let barrier_c = Arc::clone(&barrier);
3882            handles.push(std::thread::spawn(move || {
3883                barrier_c.wait();
3884                let action = StoreWalAction::CreateCollection {
3885                    name: format!("z{tx}"),
3886                };
3887                coord_c
3888                    .append_actions(std::slice::from_ref(&action))
3889                    .expect("append_actions");
3890            }));
3891        }
3892        for h in handles {
3893            h.join().expect("writer thread");
3894        }
3895
3896        // With zero window, fsync count is bounded above by WRITERS
3897        // (every caller might trigger its own drain) and below by 1
3898        // (the queue may still naturally batch under contention).
3899        // The point of the assertion is to confirm the env override
3900        // is wired through — the open() above used the env knob.
3901        let fsyncs = coord.fsync_count();
3902        assert!(fsyncs >= 1, "expected at least one fsync, got {fsyncs}");
3903
3904        std::env::remove_var("REDDB_GROUP_COMMIT_WINDOW_US");
3905        drop(coord);
3906        let _ = std::fs::remove_file(&path);
3907    }
3908
3909    /// `resolve_window` precedence: env > config.window_ms > default.
3910    #[test]
3911    fn resolve_window_precedence() {
3912        let _env = env_lock().lock().unwrap_or_else(|p| p.into_inner());
3913        // Default: window_ms=0 → adaptive 200µs floor.
3914        std::env::remove_var("REDDB_GROUP_COMMIT_WINDOW_US");
3915        let cfg = GroupCommitOptions::default();
3916        assert_eq!(
3917            StoreCommitCoordinator::resolve_window(&cfg),
3918            Duration::from_micros(DEFAULT_ADAPTIVE_WINDOW_US)
3919        );
3920
3921        // Explicit ms config wins over the default floor.
3922        let cfg_ms = GroupCommitOptions {
3923            window_ms: 5,
3924            ..GroupCommitOptions::default()
3925        };
3926        assert_eq!(
3927            StoreCommitCoordinator::resolve_window(&cfg_ms),
3928            Duration::from_millis(5)
3929        );
3930
3931        // Env override wins over both.
3932        std::env::set_var("REDDB_GROUP_COMMIT_WINDOW_US", "750");
3933        assert_eq!(
3934            StoreCommitCoordinator::resolve_window(&cfg),
3935            Duration::from_micros(750)
3936        );
3937        assert_eq!(
3938            StoreCommitCoordinator::resolve_window(&cfg_ms),
3939            Duration::from_micros(750)
3940        );
3941
3942        // Env=0 explicitly disables the floor.
3943        std::env::set_var("REDDB_GROUP_COMMIT_WINDOW_US", "0");
3944        assert_eq!(
3945            StoreCommitCoordinator::resolve_window(&cfg),
3946            Duration::from_micros(0)
3947        );
3948
3949        std::env::remove_var("REDDB_GROUP_COMMIT_WINDOW_US");
3950    }
3951}