Skip to main content

donadb_x/
commutative.rs

1//! CommutativeLog — lock-free mmap append log with parallel rayon fold at commit.
2//!
3//! # Design rationale
4//!
5//! TruthLinked (and BFT blockchains in general) execute state in strict, numbered
6//! batches called blocks.  Two properties hold that make this architecture safe:
7//!
8//! 1. **Read isolation** — every read targets the *previous* committed block.
9//!    No thread ever reads uncommitted state from the currently-active buffer.
10//!
11//! 2. **Write commutativity** — within a single block, the order in which
12//!    independent transactions write is irrelevant; the final state is identical
13//!    regardless of interleaving.
14//!
15//! Together these mean the index does not need to be updated on every `put()`.
16//! Instead, all writes land in a flat mmap log (one atomic `fetch_add` each),
17//! and at the block boundary a single parallel fold scans the dirty slice,
18//! upserts the index, and derives the new state root via XOR-accumulation of
19//! `key XOR blake3(value)` contributions.
20//!
21//! # Write path
22//! `put_versioned()`: one `fetch_add` to reserve space, one `copy_nonoverlapping`
23//! to write the packed header, key, and value.  No locks.  No hashing.
24//!
25//! # Commit path
26//! `commit_fold_until()`:
27//!   1. Parse the dirty `[committed_offset..end_offset]` slice into record descriptors.
28//!   2. `rayon::par_iter` hashes every value with blake3 in parallel.
29//!   3. Sequential dedup keeps the last write per key within this block.
30//!   4. For each surviving key, undo the old accumulator contribution and XOR in
31//!      the new one; batch-upsert into the shard index by parallel shard.
32//!   5. Advance `committed_offset` and persist it to the 8-byte header at offset 0.
33
34#![allow(unsafe_code)]
35
36use crate::{DbError, DbResult};
37use crate::index::ShardIndex;
38use memmap2::{MmapMut, MmapOptions};
39use rayon::prelude::*;
40use std::fs::OpenOptions;
41use std::path::Path;
42use std::sync::atomic::{AtomicU64, Ordering};
43
44// ─────────────────────────────────────────────────────────────────────────────
45// Record layout
46// ─────────────────────────────────────────────────────────────────────────────
47//
48// Each record written by `put_versioned()` has the following layout:
49//
50//   Offset   Size  Field
51//   ──────────────────────────────────────────────────────────
52//        0      4  magic       — always CMAG (0xC047_C047)
53//        4      4  vlen        — length of the value payload in bytes
54//        8      8  prev_offset — mmap offset of previous write for this key
55//                               (MVCC chain pointer; 0 means first write)
56//       16      8  height      — block height at write time
57//   ──────────────────────────────────────────────────────────  (24 bytes header)
58//       24     32  key         — the 32-byte key
59//       56      N  value       — raw value bytes (N == vlen)
60//
61// The first 8 bytes of the file (offset 0..8) are NOT a record header; they
62// hold the persisted committed_offset used by `open()` for crash recovery.
63
64/// Magic number that marks the start of every valid record header.
65///
66/// The distinctive bit pattern (two copies of `0xC047`) makes it easy to
67/// detect a torn write or a seek past valid data during log parsing.
68pub(crate) const CMAG: u32 = 0xC047_C047;
69
70/// Tombstone magic — written by `del_versioned` to mark a key as deleted.
71///
72/// Shares the `C047` suffix with `CMAG` so scanners that walk the log by
73/// record size can step over tombstones correctly. The fold recognises this
74/// magic and XORs out the key's accumulator contribution rather than adding
75/// a new one, then marks the index entry as `deleted = true`.
76pub(crate) const CTOMB: u32 = 0xDEAD_C047;
77
78/// Size of the packed record header in bytes (magic + vlen + prev_offset + height).
79///
80/// Equals 4 + 4 + 8 + 8 = 24.  The key (32 bytes) and value (N bytes) follow
81/// immediately after this header.
82pub(crate) const CHDR: usize = 24;
83
84// ─────────────────────────────────────────────────────────────────────────────
85// CommutativeLog
86// ─────────────────────────────────────────────────────────────────────────────
87
88pub struct CommutativeLog {
89    /// Memory-mapped file backing the log.  All record writes go here directly.
90    mmap: MmapMut,
91
92    /// Byte offset one past the last reserved (but possibly not yet written) record.
93    ///
94    /// Advanced atomically by `put_versioned()` via `fetch_add`.  May be ahead
95    /// of `committed_offset` while in-flight writes are still in progress.
96    pub write_offset: AtomicU64,
97
98    /// Byte offset one past the last record fully committed to the index.
99    ///
100    /// Represents the stable read frontier: everything below this offset is
101    /// guaranteed to be visible in the index.  Advanced by `commit_fold_until()`.
102    committed_offset: AtomicU64,
103
104    /// Total capacity of the mmap file in bytes.  Writes that would exceed this
105    /// return `DbError::LogFull`.
106    pub capacity: u64,
107
108    /// Ensures only one `commit_fold` runs at a time.
109    ///
110    /// Concurrent commits would produce incorrect accumulator values because
111    /// each commit reads the current index state before updating it; two
112    /// overlapping commits would both see the same "old" values and double-undo
113    /// contributions.
114    pub(crate) commit_lock: parking_lot::Mutex<()>,
115
116    /// Count of `put_versioned()` calls that have reserved an offset but have
117    /// not yet finished writing their data into the mmap.
118    ///
119    /// `commit_fold_until()` and `reset()` spin on this reaching zero before
120    /// they touch the region those writes are landing in.  Uses `AcqRel` on
121    /// increment and `Release` on decrement so the mmap writes are visible
122    /// before the counter drops back to zero.
123    pub(crate) inflight: std::sync::atomic::AtomicI64,
124}
125
126impl CommutativeLog {
127    /// Open (or create) a log file at `path` with a maximum size of `size` bytes.
128    ///
129    /// `path` — the file is created if it does not exist.  On restart the same
130    /// path must be passed to recover the previous committed state.
131    ///
132    /// `size` — the mmap (and underlying file) is pre-allocated to exactly this
133    /// many bytes.  Choose a value large enough to hold one full block's worth
134    /// of writes.
135    ///
136    /// # Crash recovery
137    ///
138    /// The first 8 bytes of the file always hold the last persisted
139    /// `committed_offset` as a little-endian `u64`.  On open, if that value is
140    /// within `(8, size]` both `write_offset` and `committed_offset` are
141    /// initialised to it, so the next `commit_fold` resumes from where the
142    /// previous run left off.  Any unreachable bytes between the persisted offset
143    /// and the physical end of the file are simply ignored.
144    pub fn open(path: &Path, size: u64) -> DbResult<Self> {
145        use std::os::unix::io::AsRawFd;
146
147        let f = OpenOptions::new().read(true).write(true).create(true).truncate(false).open(path)?;
148
149        // set_len / ftruncate only extends the file metadata; on sparse-file
150        // filesystems (ext4, xfs, btrfs) it does NOT allocate physical disk
151        // blocks.  A later memcpy into an unallocated page triggers a page
152        // fault that the kernel must satisfy by allocating a block.  If the
153        // disk is full at that moment, the kernel cannot complete the fault and
154        // delivers SIGBUS — killing the process with no opportunity to handle
155        // the error.
156        //
157        // posix_fallocate(2) forces the kernel to allocate real disk blocks for
158        // the entire segment up front.  If physical space is unavailable it
159        // returns ENOSPC here, at segment-creation time, where we can convert
160        // it into a normal DbError and surface it gracefully to the caller —
161        // long before any write or validator loop is running.
162        //
163        // We call set_len first so the file descriptor has the right size
164        // before fallocate inspects it (required on some older kernels).
165        f.set_len(size)?;
166        // SAFETY: f is a valid, open file descriptor for the duration of this call.
167        let rc = unsafe { libc::posix_fallocate(f.as_raw_fd(), 0, size as libc::off_t) };
168        if rc != 0 {
169            return Err(DbError::Io(std::io::Error::from_raw_os_error(rc)));
170        }
171
172        let mmap = unsafe { MmapOptions::new().map_mut(&f)? };
173        let persisted = u64::from_le_bytes(mmap[0..8].try_into().unwrap_or([0u8; 8]));
174        let start = if persisted > 8 && persisted <= size { persisted } else { 8 };
175        Ok(Self {
176            mmap,
177            write_offset:     AtomicU64::new(start),
178            committed_offset: AtomicU64::new(start),
179            capacity:         size,
180            commit_lock:      parking_lot::Mutex::new(()),
181            inflight:         std::sync::atomic::AtomicI64::new(0),
182        })
183    }
184
185    /// Append a key-value record with an MVCC chain pointer and block height.
186    ///
187    /// This is the main hot-path write operation.
188    ///
189    /// # Atomicity guarantee
190    ///
191    /// A single `fetch_add(total, AcqRel)` on `write_offset` reserves the byte
192    /// range `[off, off + total)` atomically.  Because `fetch_add` is
193    /// unconditional (no CAS loop), there is zero contention between concurrent
194    /// writers — each thread gets its own disjoint slice of the log.  On
195    /// capacity overflow the reservation is reversed with a compensating
196    /// `fetch_sub` and `DbError::LogFull` is returned.
197    ///
198    /// # In-flight tracking
199    ///
200    /// Between reserving the slot and finishing the write, `inflight` is
201    /// incremented.  `commit_fold_until()` and `reset()` spin-wait on
202    /// `inflight == 0` before reading or rewinding the log, ensuring they never
203    /// observe a partially-written record.
204    ///
205    /// # Record layout (packed `Hdr` struct)
206    ///
207    /// The 24-byte header is written as a single `repr(C, packed)` struct in one
208    /// `write_unaligned` call — one cache-line store instead of four separate
209    /// writes.  The key (32 bytes) and value (N bytes) follow immediately.
210    ///
211    /// - `prev_off` — mmap offset of the most recently committed record for this
212    ///   key, or `0` for the first write.  Patched after commit if a prior
213    ///   record is discovered during `commit_fold_until`.
214    /// - `height`   — the block height at write time, used for point-in-time queries.
215    #[inline(always)]
216    pub fn put_versioned(&self, key: [u8; 32], value: &[u8], prev_off: u64, height: u64) -> DbResult<u64> {
217        let total = (CHDR + 32 + value.len()) as u64;
218
219        // Reserve the byte range with one atomic add — no CAS spin, no lock.
220        // We claim the slot first, then check bounds.  On overflow we give it back.
221        let off = self.write_offset.fetch_add(total, Ordering::AcqRel);
222        if off + total > self.capacity {
223            // Undo the reservation so the log does not appear partially full.
224            self.write_offset.fetch_sub(total, Ordering::AcqRel);
225            return Err(DbError::LogFull);
226        }
227
228        // Signal that this slot is reserved but not yet written.
229        self.inflight.fetch_add(1, Ordering::Relaxed);
230
231        // Write the 24-byte header as a single unaligned store, then copy key and value.
232        #[repr(C, packed)]
233        struct Hdr { magic: u32, vlen: u32, prev: u64, height: u64 }
234        unsafe {
235            let base = self.mmap.as_ptr().add(off as usize) as *mut u8;
236            std::ptr::write_unaligned(base as *mut Hdr, Hdr {
237                magic:  CMAG.to_le(),
238                vlen:   (value.len() as u32).to_le(),
239                prev:   prev_off.to_le(),
240                height: height.to_le(),
241            });
242            std::ptr::copy_nonoverlapping(key.as_ptr(),   base.add(CHDR),      32);
243            std::ptr::copy_nonoverlapping(value.as_ptr(), base.add(CHDR + 32), value.len());
244        }
245
246        // Signal that the write is complete.
247        self.inflight.fetch_sub(1, Ordering::Release);
248        Ok(off)
249    }
250
251    /// Append a tombstone record for `key` at block `height`.
252    ///
253    /// A tombstone uses the tombstone magic number (`CTOMB`) and `vlen = 0` so it
254    /// occupies the minimum possible space in the log (header + key = 56 bytes).
255    /// The fold thread recognises the tombstone magic, XORs out the key's
256    /// existing accumulator contribution, and marks the index entry as deleted.
257    ///
258    /// After `commit()` + `ack.wait()`, `get(key)` returns `NotFound` and the
259    /// key is excluded from `scan_prefix` and `scan_from_reverse` results.
260    ///
261    /// # Errors
262    /// Returns [`crate::DbError::LogFull`] if the active segment has no room.
263    #[inline(always)]
264    pub fn del_versioned(&self, key: [u8; 32], prev_off: u64, height: u64) -> DbResult<u64> {
265        // A tombstone has vlen = 0; total record size = CHDR + 32 bytes.
266        let total = (CHDR + 32) as u64;
267        let off   = self.write_offset.fetch_add(total, Ordering::AcqRel);
268        if off + total > self.capacity {
269            self.write_offset.fetch_sub(total, Ordering::AcqRel);
270            return Err(crate::DbError::LogFull);
271        }
272        self.inflight.fetch_add(1, Ordering::AcqRel);
273        unsafe {
274            #[repr(C, packed)]
275            struct Hdr { magic: u32, vlen: u32, prev: u64, height: u64 }
276            let base = self.mmap.as_ptr().add(off as usize) as *mut u8;
277            let hdr  = Hdr {
278                magic:  CTOMB.to_le(),
279                vlen:   0u32.to_le(),
280                prev:   prev_off.to_le(),
281                height: height.to_le(),
282            };
283            std::ptr::write_unaligned(base as *mut Hdr, hdr);
284            std::ptr::copy_nonoverlapping(key.as_ptr(), base.add(CHDR), 32);
285        }
286        self.inflight.fetch_sub(1, Ordering::Release);
287        Ok(off)
288    }
289
290    /// Fold the entire dirty range into the index and return the new state root.
291    ///
292    /// Equivalent to `commit_fold_until(index, prev_acc, write_offset, 0)`.
293    /// The dirty range is `[committed_offset, write_offset)` at the moment of
294    /// the call — any writes that land after the snapshot of `write_offset` are
295    /// deferred to the next commit.
296    ///
297    /// Returns `(new_accumulator, blake3(new_accumulator))`.
298    pub fn commit_fold(
299        &self,
300        index: &ShardIndex,
301        prev_acc: [u8; 32],
302    ) -> DbResult<([u8; 32], [u8; 32])> {
303        self.commit_fold_until(index, prev_acc, self.write_offset.load(Ordering::Acquire), 0)
304    }
305
306    /// Fold the dirty range `[committed_offset, end_offset)` into the index.
307    ///
308    /// The `end_offset` parameter lets the caller pin a snapshot of the write
309    /// frontier so that writes that race in after the block boundary are not
310    /// accidentally included in this commit.
311    ///
312    /// `log_id` is passed through to the shard index for bookkeeping (e.g.
313    /// tracking which segment last wrote each key).
314    ///
315    /// # Step-by-step
316    ///
317    /// 1. **Acquire commit lock** — only one commit may run at a time.
318    ///
319    /// 2. **Drain in-flight writes** — spin on `inflight == 0` so every
320    ///    `put_versioned()` that reserved an offset inside `[start, end)` has
321    ///    finished writing its data.
322    ///
323    /// 3. **Parse dirty slice** — walk the mmap from `start` to `end`, checking
324    ///    the magic bytes of each record.  Collect `(offset, key, vlen)` tuples
325    ///    into a `Vec` without copying any value data (zero-copy parse).
326    ///
327    /// 4. **Parallel blake3 hashing** — `rayon::par_iter` maps each record to
328    ///    `(key, blake3(value), offset)`.  The value bytes are read via a raw
329    ///    pointer into the mmap (safe because step 2 ensures no concurrent writes).
330    ///
331    /// 5. **Sequential dedup** — sort by offset; build a `per_block_count` map
332    ///    to track how many times each key appeared; keep only the last
333    ///    `(key → value_hash, offset)` entry so the index always holds the
334    ///    highest-offset write for each key.
335    ///
336    /// 6. **Accumulator update + index upsert** — for each surviving key:
337    ///    - If the key already exists in the committed index, read its old
338    ///      value from the mmap and XOR out its `key XOR blake3(old_value)`
339    ///      contribution from `prev_acc`.
340    ///    - Patch the `prev_offset` field of the new record with the old offset
341    ///      (completing the MVCC chain).
342    ///    - XOR the new `key XOR blake3(new_value)` contribution into `new_acc`.
343    ///    - Batch updates by shard index (`key[0]`) for parallelism.
344    ///
345    /// 7. **Parallel shard upserts** — `rayon::par_iter` over the 256 shard
346    ///    batches; each shard acquires only its own lock.
347    ///
348    /// 8. **Advance committed pointer** — store `end_offset` into
349    ///    `committed_offset` and persist it to the first 8 bytes of the mmap
350    ///    for crash recovery, then issue an async flush.
351    ///
352    /// Returns `(new_accumulator, blake3(new_accumulator))`.
353    pub fn commit_fold_until(
354        &self,
355        index: &ShardIndex,
356        prev_acc: [u8; 32],
357        end_offset: u64,
358        log_id: u64,
359    ) -> DbResult<([u8; 32], [u8; 32])> {
360        let _guard = self.commit_lock.lock();
361        let start = self.committed_offset.load(Ordering::Acquire) as usize;
362        let end   = end_offset as usize;
363
364        if start >= end {
365            // Nothing to commit — return the current accumulator and its hash.
366            return Ok((prev_acc, *blake3::hash(&prev_acc).as_bytes()));
367        }
368
369        // Step 2: Wait for any put_versioned() calls that have reserved a slot inside
370        // [start, end) but have not yet finished writing their record bytes.  Without
371        // this guard, a racing writer could leave a zero-magic gap that breaks parsing.
372        while self.inflight.load(Ordering::Acquire) > 0 { std::hint::spin_loop(); }
373
374        // Step 3: Parse dirty slice into (offset, key, vlen) descriptors.
375        // Zero-copy: we record mmap offsets, not copies of the value data.
376        // Pre-allocate with a rough estimate (average record ≈ CHDR + 32 + 64 bytes)
377        // to avoid Vec reallocation during the hot parse loop.
378        let mmap_ptr    = self.mmap.as_ptr() as usize;
379        let dirty_bytes = end.saturating_sub(start);
380        let estimated   = (dirty_bytes / (CHDR + 32 + 64)).max(64);
381        let mut records: Vec<(usize, [u8; 32], usize)> = Vec::with_capacity(estimated);
382        let mut tombstones: Vec<(usize, [u8; 32])> = Vec::new();
383
384        let mut cur = start;
385        while cur + CHDR + 32 <= end {
386            let slice = &self.mmap[cur..];
387            let magic = u32::from_le_bytes(slice[0..4].try_into().unwrap());
388            if magic == CTOMB {
389                // Tombstone: vlen is always 0, record size = CHDR + 32.
390                let key: [u8; 32] = slice[CHDR..CHDR + 32].try_into().unwrap();
391                tombstones.push((cur, key));
392                cur += CHDR + 32;
393                continue;
394            }
395            if magic != CMAG { break; }
396            let vlen  = u32::from_le_bytes(slice[4..8].try_into().unwrap()) as usize;
397            let total = CHDR + 32 + vlen;
398            if cur + total > end { break; }
399            let key: [u8; 32] = slice[CHDR..CHDR + 32].try_into().unwrap();
400            records.push((cur, key, vlen));
401            cur += total;
402        }
403
404        // Step 4: Parallel blake3 hashing — value bytes read via raw pointer.
405        // Each rayon worker produces (key, blake3(value), record_offset).
406        let mut hashed: Vec<([u8; 32], [u8; 32], u64)> = records
407            .par_iter()
408            .map(|&(off, key, vlen)| {
409                let val = unsafe {
410                    std::slice::from_raw_parts(
411                        (mmap_ptr + off + CHDR + 32) as *const u8,
412                        vlen,
413                    )
414                };
415                (key, *blake3::hash(val).as_bytes(), off as u64)
416            })
417            .collect();
418
419        // Step 5: Sort by offset, then dedup — keep the highest-offset write per key.
420        // Also count how many times each key appeared in this dirty slice so the
421        // index write-count can be incremented correctly.
422        hashed.sort_unstable_by_key(|&(_, _, off)| off);
423        let mut per_block_count: ahash::AHashMap<[u8; 32], u32> =
424            ahash::AHashMap::with_capacity(hashed.len());
425        for &(key, _, _) in &hashed { *per_block_count.entry(key).or_insert(0) += 1; }
426        let mut latest: ahash::AHashMap<[u8; 32], ([u8; 32], u64)> =
427            ahash::AHashMap::with_capacity(hashed.len());
428        for (key, vh, off) in hashed {
429            latest.insert(key, (vh, off));
430        }
431
432        // Step 6: Accumulator update and MVCC chain patching.
433        //
434        // For each key that was written in this block:
435        //   a) If the key already has a committed entry, read its old value from
436        //      the mmap, hash it, and XOR the old contribution out of new_acc.
437        //   b) Patch prev_offset in the new record's header (bytes 8..16) to
438        //      point to the old record, completing the MVCC chain.
439        //   c) XOR the new contribution (key XOR blake3(new_value)) into new_acc.
440        //   d) Batch the upsert keyed by shard index (key[0]) for parallel execution.
441        //
442        // Tombstone records (CTOMB) only perform step (a) — they XOR out the
443        // old contribution and mark the entry deleted, but add nothing new.
444        //
445        // The old value hash is read from entry.value_hash (cached in the index
446        // at the previous upsert) rather than re-reading from the mmap.
447        
448        // Type alias for shard batch entries to reduce complexity
449        type ShardBatchEntry = (
450            [u8; 32],  // key
451            [u8; 32],  // value_hash
452            u64,       // offset
453            u32,       // write_count
454            bool,      // deleted
455        );
456        
457        let mut shard_batches: [Vec<ShardBatchEntry>; 256] =
458            std::array::from_fn(|_| Vec::new());
459
460        let mut new_acc = prev_acc;
461
462        // Process tombstones first: a put + delete in the same batch ends up deleted.
463        for &(off, key) in &tombstones {
464            if let Some(old_entry) = index.get_entry(&key) {
465                if !old_entry.deleted {
466                    // XOR out the old contribution using the cached value hash.
467                    for i in 0..32 { new_acc[i] ^= key[i] ^ old_entry.value_hash[i]; }
468                }
469            }
470            let wc = index.count(&key) + 1;
471            shard_batches[key[0] as usize].push((key, [0u8; 32], off as u64, wc, true));
472        }
473
474        for (&key, &(new_vh, off)) in &latest {
475            let mut prev_offset = 0u64;
476            if let Some(old_entry) = index.get_entry(&key) {
477                if !old_entry.deleted {
478                    // XOR out old contribution using the cached hash — no mmap read.
479                    for i in 0..32 { new_acc[i] ^= key[i] ^ old_entry.value_hash[i]; }
480                }
481                prev_offset = old_entry.offset;
482            }
483            // XOR in the new contribution.
484            for i in 0..32 { new_acc[i] ^= key[i] ^ new_vh[i]; }
485
486            // Patch prev_offset into the record header at bytes 8..16.
487            if prev_offset > 0 {
488                unsafe {
489                    let dst = (mmap_ptr + off as usize + 8) as *mut u8;
490                    std::ptr::copy_nonoverlapping(prev_offset.to_le_bytes().as_ptr(), dst, 8);
491                }
492            }
493
494            let block_puts = per_block_count.get(&key).copied().unwrap_or(1);
495            let wc         = index.count(&key) + block_puts;
496            let is_deleted = tombstones.iter().any(|(_, k)| k == &key);
497            shard_batches[key[0] as usize].push((key, new_vh, off, wc, is_deleted));
498        }
499
500        // Step 7: Parallel shard upserts — store value_hash so future folds
501        // can XOR out correctly without re-reading the mmap.
502        shard_batches.par_iter_mut().enumerate().for_each(|(shard_idx, batch)| {
503            if !batch.is_empty() {
504                let shard = &index.shards[shard_idx];
505                for &(key, vh, off, wc, deleted) in batch.iter() {
506                    shard.upsert(key, off, wc, log_id, deleted, vh);
507                }
508            }
509        });
510
511        let root = *blake3::hash(&new_acc).as_bytes();
512
513        // Step 8: Advance the committed pointer and persist it for crash recovery.
514        self.committed_offset.store(end as u64, Ordering::Release);
515        unsafe {
516            let dst   = self.mmap.as_ptr() as *mut u8;
517            let bytes = (end as u64).to_le_bytes();
518            std::ptr::copy_nonoverlapping(bytes.as_ptr(), dst, 8);
519        }
520        let _ = self.mmap.flush_async();
521
522        Ok((new_acc, root))
523    }
524
525    /// Issue an asynchronous flush of the mmap to the underlying file.
526    ///
527    /// The OS will write dirty pages back to disk in the background.  Use this
528    /// after `commit_fold_until` to reduce the window of data loss on a crash.
529    pub fn flush(&self) { let _ = self.mmap.flush_async(); }
530
531    /// Return the current write frontier (one past the last reserved byte).
532    ///
533    /// May be ahead of `committed_offset` while in-flight writes are landing.
534    pub fn write_offset(&self) -> u64 { self.write_offset.load(Ordering::Acquire) }
535
536    /// Return the current committed frontier (one past the last folded byte).
537    ///
538    /// Everything below this offset is reflected in the index.
539    pub fn committed_offset(&self) -> u64 { self.committed_offset.load(Ordering::Acquire) }
540
541    /// Return the raw base pointer of the mmap region.
542    ///
543    /// Used for zero-copy reads during log replay and by `commit_fold_until`
544    /// to hash value bytes without copying them.
545    pub fn mmap_ptr(&self) -> *const u8 { self.mmap.as_ptr() }
546
547    /// Reset the log for reuse at the start of the next block.
548    ///
549    /// Spins until all in-flight `put_versioned()` calls have finished writing,
550    /// then rewinds both `write_offset` and `committed_offset` to 8 (just past
551    /// the 8-byte crash-recovery header) and zeroes the header bytes so the
552    /// next `open()` sees a clean starting offset.
553    pub fn reset(&self) {
554        while self.inflight.load(Ordering::Acquire) > 0 {
555            std::hint::spin_loop();
556        }
557        self.write_offset.store(8, Ordering::Release);
558        self.committed_offset.store(8, Ordering::Release);
559        unsafe { std::ptr::write_bytes(self.mmap.as_ptr() as *mut u8, 0, 8); }
560    }
561
562    /// Forcibly advance `committed_offset` to `off`.
563    ///
564    /// Also advances `write_offset` if it is currently behind `off`, ensuring
565    /// the invariant `write_offset >= committed_offset` always holds.
566    /// Used during segment replay to restore the committed frontier without
567    /// re-running a fold.
568    pub fn set_committed(&self, off: u64) {
569        self.committed_offset.store(off, Ordering::Release);
570        let woff = self.write_offset.load(Ordering::Acquire);
571        if off > woff { self.write_offset.store(off, Ordering::Release); }
572    }
573
574    /// Scan the **unflushed** region `[committed_offset, write_offset)` for the
575    /// last write to `key`, returning its value if found.
576    ///
577    /// This covers the gap between the most recent fold and the current
578    /// write frontier: writes that have landed in the mmap via `put_versioned`
579    /// but whose index entries have not yet been committed by a fold pass.
580    ///
581    /// The scan is strictly bounded to the current block's unflushed bytes.
582    /// For well-behaved workloads this is small (one block's worth of writes).
583    /// The last occurrence of `key` in the region is returned so that repeated
584    /// writes to the same key within a block see the most recent value.
585    ///
586    /// Walks forward through the unflushed window rather than backwards because
587    /// in-flight writes do not yet have `prev_offset` MVCC chain pointers
588    /// patched in (that happens during the fold).
589    pub(crate) fn scan_unflushed(&self, key: &[u8; 32]) -> Option<Vec<u8>> {
590        let committed = self.committed_offset.load(Ordering::Acquire) as usize;
591        let written   = self.write_offset.load(Ordering::Acquire) as usize;
592        if written <= committed { return None; }
593
594        let cap = self.capacity as usize;
595        let ptr = self.mmap.as_ptr();
596        let mut best: Option<Vec<u8>> = None;
597        let mut cur = committed;
598
599        while cur + CHDR + 32 <= written {
600            let magic = u32::from_le_bytes(unsafe {
601                std::slice::from_raw_parts(ptr.add(cur), 4)
602            }.try_into().unwrap());
603
604            if magic == CTOMB {
605                let rec_key: [u8; 32] = unsafe {
606                    std::slice::from_raw_parts(ptr.add(cur + CHDR), 32)
607                }.try_into().unwrap();
608                if rec_key == *key { best = None; } // tombstone wins
609                cur += CHDR + 32;
610                continue;
611            }
612
613            if magic != CMAG { break; }
614
615            let vlen = u32::from_le_bytes(unsafe {
616                std::slice::from_raw_parts(ptr.add(cur + 4), 4)
617            }.try_into().unwrap()) as usize;
618            let total = CHDR + 32 + vlen;
619            if cur + total > written || cur + total > cap { break; }
620
621            let rec_key: [u8; 32] = unsafe {
622                std::slice::from_raw_parts(ptr.add(cur + CHDR), 32)
623            }.try_into().unwrap();
624
625            if rec_key == *key {
626                best = Some(unsafe {
627                    std::slice::from_raw_parts(ptr.add(cur + CHDR + 32), vlen).to_vec()
628                });
629            }
630            cur += total;
631        }
632        best
633    }
634
635    /// Scan the entire unflushed region and return the **last** value seen for
636    /// every key, as `(key, value)` pairs.
637    ///
638    /// Tombstones are returned as `(key, empty Vec)` so callers can distinguish
639    /// "deleted in this batch" from "not present". Used by `scan_prefix` and
640    /// `scan_from_reverse` to overlay in-flight writes on top of the committed
641    /// index without requiring a fold.
642    pub(crate) fn scan_all_unflushed(&self) -> Vec<([u8; 32], Vec<u8>)> {
643        let committed = self.committed_offset.load(Ordering::Acquire) as usize;
644        let written   = self.write_offset.load(Ordering::Acquire) as usize;
645        if written <= committed { return Vec::new(); }
646
647        let cap = self.capacity as usize;
648        let ptr = self.mmap.as_ptr();
649        // Use a map to keep only the last write per key in this window.
650        let mut map: ahash::AHashMap<[u8; 32], Vec<u8>> = ahash::AHashMap::new();
651        let mut cur = committed;
652
653        while cur + CHDR + 32 <= written {
654            let magic = u32::from_le_bytes(unsafe {
655                std::slice::from_raw_parts(ptr.add(cur), 4)
656            }.try_into().unwrap());
657
658            if magic == CTOMB {
659                let key: [u8; 32] = unsafe {
660                    std::slice::from_raw_parts(ptr.add(cur + CHDR), 32)
661                }.try_into().unwrap();
662                map.insert(key, Vec::new()); // empty = tombstone
663                cur += CHDR + 32;
664                continue;
665            }
666
667            if magic != CMAG { break; }
668
669            let vlen = u32::from_le_bytes(unsafe {
670                std::slice::from_raw_parts(ptr.add(cur + 4), 4)
671            }.try_into().unwrap()) as usize;
672            let total = CHDR + 32 + vlen;
673            if cur + total > written || cur + total > cap { break; }
674
675            let key: [u8; 32] = unsafe {
676                std::slice::from_raw_parts(ptr.add(cur + CHDR), 32)
677            }.try_into().unwrap();
678            let val = unsafe {
679                std::slice::from_raw_parts(ptr.add(cur + CHDR + 32), vlen).to_vec()
680            };
681            map.insert(key, val);
682            cur += total;
683        }
684        map.into_iter().collect()
685    }
686}
687
688// ── MmapRef — zero-copy value reference ──────────────────────────────────────
689
690/// A zero-copy handle to a value stored in a `CommutativeLog` mmap.
691///
692/// On the hot path (committed index hit) this holds an `Arc<CommutativeLog>`
693/// and a raw pointer into its mmap — no heap allocation. For values that live
694/// in sealed segments or the unflushed active log, it falls back to a `Vec<u8>`
695/// heap buffer. Either way callers use it identically via `Deref<Target=[u8]>`.
696pub struct MmapRef {
697    /// Keeps the mmap mapping alive when using the zero-copy path.
698    _log:  Option<std::sync::Arc<CommutativeLog>>,
699    /// Pointer into the mmap (zero-copy path) or into the heap Vec (fallback).
700    ptr:   *const u8,
701    /// Length of the value in bytes.
702    len:   usize,
703    /// Heap-allocated buffer for the fallback path. Kept alive here so `ptr`
704    /// remains valid for the lifetime of `MmapRef`.
705    _heap: Option<Vec<u8>>,
706}
707
708// SAFETY: CommutativeLog is Send + Sync; the pointed-to region is immutable.
709unsafe impl Send for MmapRef {}
710unsafe impl Sync for MmapRef {}
711
712impl MmapRef {
713    /// Wrap a heap-allocated `Vec<u8>` in a `MmapRef`.
714    ///
715    /// Used for values that come from sealed segments or the unflushed active
716    /// log, where a zero-copy mmap reference is not possible.
717    pub fn from_vec(v: Vec<u8>) -> Self {
718        let ptr = v.as_ptr();
719        let len = v.len();
720        MmapRef { _log: None, ptr, len, _heap: Some(v) }
721    }
722}
723
724impl std::ops::Deref for MmapRef {
725    type Target = [u8];
726    #[inline(always)]
727    fn deref(&self) -> &[u8] {
728        unsafe { std::slice::from_raw_parts(self.ptr, self.len) }
729    }
730}
731
732impl AsRef<[u8]> for MmapRef {
733    fn as_ref(&self) -> &[u8] { self }
734}
735
736impl std::fmt::Debug for MmapRef {
737    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
738        write!(f, "MmapRef({} bytes)", self.len)
739    }
740}
741
742impl CommutativeLog {
743    /// Return a zero-copy [`MmapRef`] for the value at `off`, or `None` if the
744    /// record is invalid or is a tombstone.
745    ///
746    /// The returned reference borrows the mmap through the `Arc` — no heap
747    /// allocation is needed. Use this on the hot read path when the caller only
748    /// needs to inspect or hash the value without keeping a copy.
749    pub fn value_ref(self: &std::sync::Arc<Self>, off: usize) -> Option<MmapRef> {
750        let cap = self.capacity as usize;
751        if off + CHDR + 32 > cap { return None; }
752        let ptr = self.mmap.as_ptr();
753        let magic = u32::from_le_bytes(unsafe {
754            std::slice::from_raw_parts(ptr.add(off), 4)
755        }.try_into().ok()?);
756        if magic != CMAG { return None; }
757        let vlen = u32::from_le_bytes(unsafe {
758            std::slice::from_raw_parts(ptr.add(off + 4), 4)
759        }.try_into().ok()?) as usize;
760        let vs = off + CHDR + 32;
761        if vs + vlen > cap { return None; }
762        Some(MmapRef {
763            _log:  Some(std::sync::Arc::clone(self)),
764            ptr:   unsafe { ptr.add(vs) },
765            len:   vlen,
766            _heap: None,
767        })
768    }
769}