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}