Skip to main content

donadb_x/
engine.rs

1//! Engine layer — the public-facing orchestrator of all five subsystems.
2//!
3//! This module owns the [`DonaDbX`] struct and wires together:
4//!
5//! - **Subsystem 2** ([`crate::commutative::CommutativeLog`]): lock-free mmap append log.
6//! - **Subsystem 3** ([`crate::dual_buffer::DualBufferEngine`]): parallel fold coordinator.
7//! - **Subsystem 4** (Rayon fold pool inside `DualBufferEngine`): parallel BLAKE3 + XOR
8//!   state-root computation, running asynchronously after each `commit()`.
9//! - **Subsystem 5** ([`crate::segment`]): segment rotation, JSON manifest, crash recovery.
10//! - **Subsystem 1** ([`ExecutionFrame`]): isolated write arena with rollback support.
11
12#![allow(unsafe_code)]
13
14use crate::commutative::{CommutativeLog, CMAG, CTOMB, CHDR};
15use crate::dual_buffer::DualBufferEngine;
16use crate::index::ShardIndex;
17use crate::segment::{Manifest, SegmentMeta, SealedSegment};
18use std::path::{Path, PathBuf};
19use std::sync::Arc;
20use std::sync::atomic::{AtomicU64, Ordering};
21
22/// Re-export so callers can use [`BlockWriter`] without importing `dual_buffer`.
23pub use crate::dual_buffer::BlockWriter;
24
25// ── Config ────────────────────────────────────────────────────────────────────
26
27/// Runtime configuration for a [`DonaDbX`] instance.
28///
29/// All fields have sensible defaults via [`Config::default()`]; most deployments
30/// only need to override `buffer_size` to match available RAM.
31///
32/// # Example
33/// ```
34/// use donadb_x::Config;
35///
36/// let cfg = Config {
37///     buffer_size:      512 << 20, // 512 MiB segments
38///     rotate_threshold: 0.75,
39///     fold_threads:     Some(4),
40/// };
41/// ```
42#[derive(Clone, Debug)]
43pub struct Config {
44    /// Maximum size of a single log segment in bytes.
45    ///
46    /// The active log is memory-mapped at this size on creation. When the fill
47    /// ratio reaches [`rotate_threshold`][Config::rotate_threshold], the segment
48    /// is sealed and a fresh one is opened.
49    ///
50    /// Default: `2 GiB` (`2 << 30`).
51    pub buffer_size: u64,
52
53    /// Fraction of [`buffer_size`][Config::buffer_size] at which segment rotation
54    /// is triggered, expressed as a value in `(0.0, 1.0]`.
55    ///
56    /// Rotation is checked after every `commit()`. Once
57    /// `write_offset / buffer_size >= rotate_threshold`, the current segment is
58    /// sealed atomically and a new active log is created.
59    ///
60    /// Default: `0.80` (rotate at 80 % full).
61    pub rotate_threshold: f64,
62
63    /// Number of threads used by the Rayon fold pool for post-commit hashing.
64    ///
65    /// `None` lets the engine choose automatically (currently half of the
66    /// logical CPU count). Set an explicit value to bound CPU usage on
67    /// shared hosts.
68    ///
69    /// Default: `None` (auto-detect).
70    pub fold_threads: Option<usize>,
71}
72
73impl Default for Config {
74    /// Returns a configuration suitable for most validator deployments:
75    /// 2 GiB segments, 80 % rotation threshold, auto-detected fold threads.
76    fn default() -> Self {
77        Self {
78            buffer_size:      2 << 30,
79            rotate_threshold: 0.80,
80            fold_threads:     None,
81        }
82    }
83}
84
85// ── DonaDbX ───────────────────────────────────────────────────────────────────
86
87/// The top-level storage engine handle.
88///
89/// `DonaDbX` is a single struct that encapsulates all five subsystems of the
90/// DonaDbX storage engine. It is `Send + Sync` and is typically shared across
91/// threads via an `Arc<DonaDbX>`.
92///
93/// Writes are lock-free on the hot path: [`put()`][DonaDbX::put] performs a
94/// single `fetch_add` to reserve space in the active log, then copies the
95/// payload in. [`commit()`][DonaDbX::commit] snapshots all shards and launches
96/// a parallel fold; the returned [`CommitAck`] lets callers wait for or poll
97/// the resulting state root.
98pub struct DonaDbX {
99    dbe: DualBufferEngine,
100    gen: AtomicU64,
101    sealed: parking_lot::RwLock<Vec<SealedSegment>>,
102    manifest: parking_lot::Mutex<Manifest>,
103    manifest_path: PathBuf,
104    dir: PathBuf,
105    cfg: Config,
106    current_height: AtomicU64,
107    /// Direct Arc to the current segment's CommutativeLog.
108    /// Updated only on segment rotation. Zero atomic overhead on the read
109    /// hot path — no ArcSwap load, no refcount bump per get().
110    segment_log: arc_swap::ArcSwap<CommutativeLog>,
111}
112
113// ── CommitAck ─────────────────────────────────────────────────────────────────
114
115/// Handle returned by [`DonaDbX::commit()`].
116///
117/// After a `commit()` call, the active log is atomically swapped and the Rayon
118/// fold pool begins computing the new state root in the background. `CommitAck`
119/// lets callers synchronise with that computation.
120///
121/// - Call [`wait()`][CommitAck::wait] to **block** until the fold completes and
122///   obtain the 32-byte state root. Use this at the end of a block when you need
123///   the root before producing a block header.
124/// - Call [`try_poll()`][CommitAck::try_poll] to **check without blocking**
125///   whether the fold has finished. Useful in async runtimes or polling loops
126///   where blocking is undesirable.
127///
128/// The `#[must_use]` attribute ensures that accidentally discarding an ack
129/// (and therefore never waiting for the fold) produces a compiler warning.
130#[must_use = "call .wait() to obtain the state root once the fold completes"]
131pub struct CommitAck {
132    rx: std::sync::mpsc::Receiver<[u8; 32]>,
133}
134
135impl CommitAck {
136    /// Block until the async fold completes and return the 32-byte state root.
137    ///
138    /// If the fold thread panicked or the channel was dropped, returns an
139    /// all-zero sentinel value rather than propagating the error.
140    pub fn wait(self) -> [u8; 32] {
141        self.rx.recv().unwrap_or([0u8; 32])
142    }
143
144    /// Non-blocking poll. Returns `Some(root)` if the fold has finished,
145    /// `None` if it is still running.
146    pub fn try_poll(&self) -> Option<[u8; 32]> {
147        self.rx.try_recv().ok()
148    }
149}
150
151// ── DonaDbX impl ──────────────────────────────────────────────────────────────
152
153impl DonaDbX {
154    // ── Subsystem 5: open + crash recovery ────────────────────────────────────
155
156    /// Open or create a DonaDbX database at `dir`.
157    ///
158    /// # First call (new database)
159    /// The directory is created if it does not exist. A fresh `seg_active.log`
160    /// is memory-mapped at `cfg.buffer_size` bytes, and a `manifest.json` is
161    /// written listing it as the sole active segment.
162    ///
163    /// # Reopen (existing database)
164    /// The manifest is read to discover all sealed segments, which are
165    /// memory-mapped read-only. The active log is then replayed: every record
166    /// whose offset falls below the persisted committed-offset is re-inserted
167    /// into the in-memory index and XOR Merkle accumulator. This restores full
168    /// query capability without a separate write-ahead log.
169    ///
170    /// # Crash recovery
171    /// If the process crashed mid-write, the active log may contain a partial
172    /// record beyond the committed-offset. The replay scan stops at the first
173    /// corrupt or incomplete record, leaving only fully written data visible.
174    /// No manual repair step is required.
175    ///
176    /// # Errors
177    /// Returns [`crate::DbError::Io`] if the directory cannot be created, any
178    /// segment file cannot be opened, or the manifest cannot be read or written.
179    /// Returns [`crate::DbError::Corrupt`] if a mandatory segment file is
180    /// structurally invalid.
181    pub fn open(dir: impl AsRef<Path>, cfg: Config) -> crate::DbResult<Self> {
182        let dir = dir.as_ref();
183        std::fs::create_dir_all(dir)?;
184
185        let manifest_path = dir.join("manifest.json");
186        let mut manifest  = Manifest::load(&manifest_path)?;
187
188        // Memory-map every sealed segment listed in the manifest.
189        let mut sealed_vec: Vec<SealedSegment> = Vec::new();
190        for meta in manifest.segments.iter().filter(|m| m.sealed) {
191            sealed_vec.push(SealedSegment::open(meta.clone())?);
192        }
193
194        // Open the durable active log file (seg_active.log).
195        let active_path = dir.join("seg_active.log");
196        let active_log  = Arc::new(CommutativeLog::open(&active_path, cfg.buffer_size)?);
197
198        // Construct the DualBufferEngine, passing the active log as its initial buffer.
199        let dbe = DualBufferEngine::open(dir, cfg.buffer_size, Arc::clone(&active_log))?;
200
201        // Replay committed records from the active log to rebuild the index and
202        // Merkle accumulator after a restart or crash.
203        {
204            Self::replay_into(&active_log, 0, &dbe.index, &dbe.merkle)?;
205        }
206        // The local active_log Arc is dropped here; DualBufferEngine holds the
207        // sole remaining reference via its internal ArcSwap.
208
209        // Ensure the manifest always has an entry for the active segment.
210        if !manifest.segments.iter().any(|m| !m.sealed) {
211            manifest.segments.push(SegmentMeta {
212                id:         manifest.next_id,
213                path:       dir.join("seg_active.log").to_string_lossy().into(),
214                sealed:     false,
215                used:       0,
216                min_height: 0,
217                max_height: u64::MAX,
218            });
219            manifest.next_id += 1;
220            manifest.save(&manifest_path)?;
221        }
222
223        Ok(Self {
224            dbe,
225            gen:           AtomicU64::new(0),
226            sealed:        parking_lot::RwLock::new(sealed_vec),
227            manifest:      parking_lot::Mutex::new(manifest),
228            manifest_path,
229            dir:           dir.to_owned(),
230            cfg,
231            current_height: AtomicU64::new(0),
232            segment_log:   arc_swap::ArcSwap::from(Arc::clone(&active_log)),
233        })
234    }
235
236    // ── Subsystem 2: lock-free write ──────────────────────────────────────────
237
238    /// Append a key-value pair to the active log.
239    ///
240    /// This method is **truly lock-free**: it performs a single atomic
241    /// `fetch_add` to reserve a slot in the memory-mapped log, then copies
242    /// the key and value into that slot with `memcpy`. No mutex or spin-lock
243    /// is acquired on this path.
244    ///
245    /// The `prev_offset` field in the record header is written as `0` here.
246    /// The fold pass, which runs after [`commit()`][Self::commit], patches it
247    /// to the previous offset for the same key using the index it already holds.
248    /// This keeps the write path free of any index reads or shard locks.
249    ///
250    /// # Parameters
251    /// - `key`: the 32-byte key to write.
252    /// - `value`: arbitrary byte slice to associate with the key.
253    ///
254    /// # Returns
255    /// The byte offset in the log at which this record was written. Offsets are
256    /// stable for the lifetime of the segment and can be used with
257    /// [`get_at()`][Self::get_at] for direct MVCC access.
258    ///
259    /// # Errors
260    /// Returns [`crate::DbError::LogFull`] if the active segment has no room
261    /// for the record.
262    #[inline(always)]
263    pub fn put(&self, key: [u8; 32], value: &[u8]) -> crate::DbResult<u64> {
264        let height = self.current_height.load(Ordering::Acquire);
265        self.dbe.active.load().put_versioned(key, value, 0, height)
266    }
267
268    // ── Subsystem 3 + 4: commit — snapshot + async parallel fold ─────────────
269
270    /// Commit all pending writes for block `height` and launch the async fold.
271    ///
272    /// Snapshots the write frontier across all shards and dispatches a parallel
273    /// fold operation:
274    ///
275    /// 1. **Snapshot frontiers** — capture `write_offset` for each shard
276    /// 2. **Parallel fold** — the Rayon thread pool processes all shards
277    ///    simultaneously, computing `BLAKE3(value)` for every record, updating
278    ///    the index with correct `prev_offset` MVCC chain pointers, and
279    ///    XOR-folding results into the commutative state root
280    ///
281    /// The fold runs on a dedicated thread pool without blocking the caller.
282    ///
283    /// # Returns
284    /// A [`CommitAck`] whose [`wait()`][CommitAck::wait] method blocks until
285    /// the fold finishes and returns the new 32-byte state root.
286    ///
287    /// # Errors
288    /// Returns [`crate::DbError::Io`] if segment rotation fails.
289    pub fn commit(&self, height: u64) -> crate::DbResult<CommitAck> {
290        self.current_height.store(height, Ordering::Release);
291        let cur_gen     = self.gen.load(Ordering::Acquire);
292        let sealed_used = self.dbe.active.load().write_offset();
293
294        // Snapshot all shards and dispatch parallel fold
295        let fold_ack = self.dbe.swap(cur_gen)?;
296
297        // After the swap, check whether the sealed buffer has crossed the
298        // rotation threshold and rotate if so.
299        if sealed_used as f64 / self.cfg.buffer_size as f64 >= self.cfg.rotate_threshold {
300            self.rotate(height, sealed_used)?;
301        }
302
303        Ok(CommitAck { rx: fold_ack.into_rx() })
304    }
305
306    // ── Reads ─────────────────────────────────────────────────────────────────
307
308    /// Look up the latest committed value for `key`.
309    ///
310    /// Search order (optimized for minimal latency):
311    /// 0. **Value cache** — O(1) lock-free CLOCK cache, zero mmap access
312    /// 1. **Index** — O(1) lock-free DashMap lookup per shard
313    /// 2. **Active log unflushed region** — linear scan for writes since last fold
314    /// 3. **Sealed segments** — historical data from rotated segments
315    ///
316    /// The index entry's `log_id` is compared against the current generation
317    /// counter before dereferencing the offset. If they differ, the entry
318    /// belongs to a segment that has since been rotated.
319    ///
320    /// # Errors
321    /// Returns [`crate::DbError::NotFound`] if the key has never been written.
322    /// Returns [`crate::DbError::Corrupt`] if the record at the stored offset
323    /// fails its magic-number check.
324    pub fn get(&self, key: &[u8; 32]) -> crate::DbResult<Vec<u8>> {
325        // Step 0: value cache — O(1), zero mmap access, zero page faults.
326        // Populated by the fold thread on each commit(). A cache hit returns
327        // immediately without consulting the index or touching the mmap.
328        if let Some(entry) = self.dbe.index.get_entry(key) {
329            if entry.deleted { return Err(crate::DbError::NotFound); }
330            if let Some(cached) = self.dbe.vcache.get(key) {
331                return Ok(cached.to_vec());
332            }
333        }
334
335        // Step 1: index — O(1), covers all folded writes.
336        if let Some(entry) = self.dbe.index.get_entry(key) {
337            if entry.deleted { return Err(crate::DbError::NotFound); }
338            if entry.log_id == self.gen.load(Ordering::Acquire) {
339                let log = self.segment_log.load();
340                if let Ok(v) = self.read_from_log(&log, entry.offset as usize) {
341                    return Ok(v);
342                }
343            }
344        }
345
346        // Step 2: unflushed active-log scan — writes since last fold.
347        {
348            let active = self.dbe.active.load();
349            if let Some(v) = active.scan_unflushed(key) {
350                return Ok(v);
351            }
352        }
353
354        // Step 3: sealed segments — rotated history.
355        for seg in self.sealed.read().iter().rev() {
356            if let Some(v) = seg.get_at(key, seg.meta.max_height) {
357                return Ok(v);
358            }
359        }
360        Err(crate::DbError::NotFound)
361    }
362
363    /// Zero-copy read — return a `MmapRef` pointing directly into the mmap.
364    ///
365    /// Same lookup order as [`get`][Self::get] but avoids heap allocation
366    /// on the happy path by returning a reference into the mmap.
367    ///
368    /// Falls back to a `Vec`-backed allocation (wrapped in `MmapRef`) only
369    /// for values that live in sealed segments (sealed segment reads already
370    /// return `Vec<u8>`).
371    ///
372    /// # Errors
373    /// Returns [`crate::DbError::NotFound`] if the key has never been written
374    /// or was deleted.
375    pub fn get_slice(&self, key: &[u8; 32]) -> crate::DbResult<crate::commutative::MmapRef> {
376        if let Some(entry) = self.dbe.index.get_entry(key) {
377            if entry.deleted { return Err(crate::DbError::NotFound); }
378            if entry.log_id == self.gen.load(Ordering::Acquire) {
379                let committed = self.segment_log.load();
380                if let Some(r) = committed.value_ref(entry.offset as usize) {
381                    return Ok(r);
382                }
383            }
384        }
385        // Unflushed active-log scan
386        {
387            let active = self.dbe.active.load();
388            if let Some(v) = active.scan_unflushed(key) {
389                return Ok(crate::commutative::MmapRef::from_vec(v));
390            }
391        }
392        // Sealed segments
393        for seg in self.sealed.read().iter().rev() {
394            if let Some(v) = seg.get_at(key, seg.meta.max_height) {
395                return Ok(crate::commutative::MmapRef::from_vec(v));
396            }
397        }
398        Err(crate::DbError::NotFound)
399    }
400
401    /// Look up the value of `key` as it was at block `height`.
402    ///
403    /// Performs an **MVCC chain walk**: each log record stores a `prev_offset`
404    /// pointer to the preceding version of the same key. This method follows
405    /// the chain from the most recent entry backwards until it finds a record
406    /// whose block height is ≤ `height`.
407    ///
408    /// `height` semantics: a record written during `commit(h)` has height `h`.
409    /// Requesting `get_at(key, h)` returns the value that was current at the
410    /// end of block `h` — i.e., the most recent write with `record.height <= h`.
411    ///
412    /// The search tries the active log first (if its `min_height <= height`),
413    /// then walks sealed segments in reverse chronological order.
414    ///
415    /// # Errors
416    /// Returns [`crate::DbError::NotFound`] if no version of the key exists at
417    /// or before `height`.
418    pub fn get_at(&self, key: &[u8; 32], height: u64) -> crate::DbResult<Vec<u8>> {
419        let active_min = self.manifest.lock().segments.iter()
420            .find(|m| !m.sealed)
421            .map(|m| m.min_height)
422            .unwrap_or(0);
423
424        if active_min <= height {
425            if let Some(entry) = self.dbe.index.get_entry(key) {
426                if entry.log_id == self.gen.load(Ordering::Acquire) {
427                    let log = self.segment_log.load();
428                    if let Ok(v) = self.chain_walk(&log, entry.offset as usize, height) {
429                        return Ok(v);
430                    }
431                }
432            }
433        }
434
435        for seg in self.sealed.read().iter().rev() {
436            if seg.meta.min_height > height {
437                continue;
438            }
439            if let Some(v) = seg.get_at(key, height) {
440                return Ok(v);
441            }
442        }
443        Err(crate::DbError::NotFound)
444    }
445
446    /// Mark `key` as deleted in the active log.
447    ///
448    /// Appends a tombstone record (`CTOMB` magic, `vlen = 0`) to the active log.
449    /// After `commit()` + `ack.wait()`:
450    /// - [`get(key)`][Self::get] returns `NotFound`.
451    /// - The key's `key XOR BLAKE3(value)` contribution is XORed out of the
452    ///   state root accumulator, keeping the root consistent.
453    /// - The key is excluded from [`scan_prefix`][Self::scan_prefix] results.
454    ///
455    /// Deletion is reversible: a subsequent `put(key, new_value)` in a later
456    /// block resurrects the key normally.
457    ///
458    /// # Errors
459    /// Returns [`crate::DbError::LogFull`] if the active segment has no room.
460    #[inline(always)]
461    pub fn delete(&self, key: [u8; 32]) -> crate::DbResult<u64> {
462        let height = self.current_height.load(Ordering::Acquire);
463        // Remove from the value cache immediately so concurrent get() calls
464        // see NotFound without waiting for the next fold.
465        self.dbe.vcache.remove(&key);
466        self.dbe.active.load().del_versioned(key, 0, height)
467    }
468
469    /// Return all committed key-value pairs whose key starts with `prefix`.
470    ///
471    /// Results are collected from the in-memory index (folded writes) and the
472    /// active-log unflushed region (writes since the last `commit()`), then
473    /// sorted by key in ascending order.
474    ///
475    /// Keys that were deleted (tombstoned) are excluded. The prefix is matched
476    /// byte-for-byte against the full 32-byte key.
477    ///
478    /// # Performance
479    /// O(n) over the number of live index entries — the index is unordered
480    /// (shard hash maps), so a full scan is required. For workloads that call
481    /// `scan_prefix` frequently consider keeping a sorted secondary structure;
482    /// for consensus storage (one scan per block boundary) this is fine.
483    pub fn scan_prefix(&self, prefix: &[u8]) -> crate::DbResult<Vec<([u8; 32], Vec<u8>)>> {
484        let mut results: Vec<([u8; 32], Vec<u8>)> = Vec::new();
485
486        // Step 1: collect all non-deleted index entries whose key matches prefix.
487        // Use get() for value retrieval — it handles committed log, active-log
488        // unflushed region, and sealed segments correctly across any number of
489        // prior commits.
490        for entry in self.dbe.index.all_entries() {
491            if entry.deleted { continue; }
492            if !entry.key.starts_with(prefix) { continue; }
493            if let Ok(v) = self.get(&entry.key) {
494                results.push((entry.key, v));
495            }
496        }
497
498        // Step 2: overlay unflushed active-log writes — covers keys written in
499        // the current uncommitted batch that are not yet in the index.
500        let active = self.dbe.active.load();
501        for (key, val) in active.scan_all_unflushed() {
502            if !key.starts_with(prefix) { continue; }
503            results.retain(|(k, _)| k != &key);
504            if !val.is_empty() {
505                results.push((key, val));
506            }
507        }
508
509        results.sort_unstable_by_key(|(k, _)| *k);
510        Ok(results)
511    }
512
513    pub fn scan_from_reverse(&self, start: &[u8; 32]) -> crate::DbResult<Vec<([u8; 32], Vec<u8>)>> {
514        let mut results: Vec<([u8; 32], Vec<u8>)> = Vec::new();
515
516        for entry in self.dbe.index.all_entries() {
517            if entry.deleted { continue; }
518            if entry.key.as_ref() > start.as_ref() { continue; }
519            if let Ok(v) = self.get(&entry.key) {
520                results.push((entry.key, v));
521            }
522        }
523
524        results.sort_unstable_by(|(a, _), (b, _)| b.cmp(a));
525        Ok(results)
526    }
527
528    /// Return the current 32-byte state root.
529    ///
530    /// The state root is a commutative XOR accumulator over `key XOR BLAKE3(value)`
531    /// for every key currently in the index. It is updated incrementally by the
532    /// fold pass after each [`commit()`][Self::commit], so this call is **O(1)** —
533    /// it simply reads the accumulator value from memory.
534    ///
535    /// The root covers exactly the set of keys visible to [`get()`][Self::get]:
536    /// all committed writes across the active log and all sealed segments.
537    pub fn state_root(&self) -> [u8; 32] {
538        self.dbe.state_root()
539    }
540
541    /// Return the number of distinct keys in the index.
542    ///
543    /// This counts unique keys across all shards of the in-memory [`ShardIndex`].
544    /// It reflects the state after the last completed fold, not pending writes
545    /// in the active buffer.
546    pub fn len(&self) -> usize {
547        self.dbe.index.len()
548    }
549
550    /// Returns `true` if the index contains no keys.
551    ///
552    /// This checks whether the in-memory index is empty after the last
553    /// completed fold, not including pending writes in the active buffer.
554    pub fn is_empty(&self) -> bool {
555        self.dbe.index.is_empty()
556    }
557
558    /// Return the number of index shards (informational).
559    pub fn num_shards(&self) -> usize {
560        self.dbe.num_shards()
561    }
562
563    /// Acquire a per-thread write handle for shard `shard_id`.
564    ///
565    /// `BlockWriter` holds a direct `Arc<CommutativeLog>`, bypassing the
566    /// `ArcSwap` load on every write. This makes it suitable for tight loops
567    /// where the overhead of an atomic pointer load per `put()` would be
568    /// measurable.
569    ///
570    /// **Shard semantics**: `shard_id` is purely advisory — it is used by the
571    /// `DualBufferEngine` to partition keys across index shards for reduced
572    /// lock contention. A thread should pick a consistent shard for the
573    /// duration of a block. The number of valid shard IDs is
574    /// [`num_shards()`][Self::num_shards]; values outside that range are
575    /// wrapped modulo the shard count.
576    ///
577    /// **One per thread per block**: do not share a `BlockWriter` between
578    /// threads. Obtain a new writer at the start of each block, as the
579    /// underlying `Arc<CommutativeLog>` is replaced on every
580    /// [`commit()`][Self::commit].
581    pub fn writer(&self, shard_id: usize) -> BlockWriter {
582        let height = self.current_height.load(Ordering::Acquire);
583        self.dbe.writer(shard_id, height)
584    }
585
586    // ── Subsystem 1: ExecutionFrame factory ───────────────────────────────────
587
588    /// Begin an isolated write frame.
589    ///
590    /// An `ExecutionFrame` provides **read isolation**: writes made through the
591    /// frame are appended to the active log (and are therefore durable once
592    /// [`commit()`][ExecutionFrame::commit] is called), but they are not
593    /// inserted into the index until the fold pass runs after the commit.
594    /// Concurrent [`get()`][Self::get] calls on the parent `DonaDbX` will not
595    /// observe the frame's writes until after the commit and fold complete.
596    ///
597    /// If the frame is no longer needed, call [`abort()`][ExecutionFrame::abort]
598    /// to zero out the reserved log region and roll back the write offset as if
599    /// the writes never happened.
600    pub fn begin_frame(&self) -> ExecutionFrame<'_> {
601        ExecutionFrame {
602            db:     self,
603            start:  self.dbe.active.load().write_offset(),
604            log_id: self.gen.load(Ordering::Acquire),
605        }
606    }
607
608    // ── Private helpers ───────────────────────────────────────────────────────
609
610    /// Read the value from the committed log at byte offset `off`.
611    ///
612    /// Validates the record magic number and length fields before copying the
613    /// value bytes into a new `Vec`. Returns `Corrupt` on any structural error.
614    #[cfg(test)]
615    fn read_committed(&self, off: usize) -> crate::DbResult<Vec<u8>> {
616        self.read_from_log(&self.segment_log.load(), off)
617    }
618
619    /// Read the value at byte offset `off` from an arbitrary `CommutativeLog`.
620    fn read_from_log(&self, log: &CommutativeLog, off: usize) -> crate::DbResult<Vec<u8>> {
621        let cap = log.capacity as usize;
622        if off + CHDR + 32 > cap {
623            return Err(crate::DbError::Corrupt);
624        }
625        let ptr = log.mmap_ptr();
626        let magic = u32::from_le_bytes(unsafe {
627            std::slice::from_raw_parts(ptr.add(off), 4)
628        }.try_into().unwrap());
629        if magic != CMAG {
630            return Err(crate::DbError::Corrupt);
631        }
632        let vlen = u32::from_le_bytes(unsafe {
633            std::slice::from_raw_parts(ptr.add(off + 4), 4)
634        }.try_into().unwrap()) as usize;
635        let vs = off + CHDR + 32;
636        if vs + vlen > cap {
637            return Err(crate::DbError::Corrupt);
638        }
639        Ok(unsafe { std::slice::from_raw_parts(ptr.add(vs), vlen).to_vec() })
640    }
641
642    /// Walk the MVCC `prev_offset` chain to find the value at or before `target` height.
643    ///
644    /// Each record in the log stores a `prev_offset` pointer (8 bytes at header
645    /// offset 8) to the previous write for the same key, and a block height (8
646    /// bytes at header offset 16). This method follows the chain until it finds
647    /// a record whose height ≤ `target` and returns its value, or returns
648    /// `NotFound` if no such record exists.
649    fn chain_walk(
650        &self,
651        log: &CommutativeLog,
652        start: usize,
653        target: u64,
654    ) -> crate::DbResult<Vec<u8>> {
655        let cap = log.capacity as usize;
656        let ptr = log.mmap_ptr();
657        let mut off = start;
658        loop {
659            if off + CHDR + 32 > cap {
660                return Err(crate::DbError::Corrupt);
661            }
662            let magic = u32::from_le_bytes(unsafe {
663                std::slice::from_raw_parts(ptr.add(off), 4)
664            }.try_into().unwrap());
665            if magic != CMAG {
666                return Err(crate::DbError::Corrupt);
667            }
668            let vlen = u32::from_le_bytes(unsafe {
669                std::slice::from_raw_parts(ptr.add(off + 4), 4)
670            }.try_into().unwrap()) as usize;
671            let prev = u64::from_le_bytes(unsafe {
672                std::slice::from_raw_parts(ptr.add(off + 8), 8)
673            }.try_into().unwrap());
674            let rec_h = u64::from_le_bytes(unsafe {
675                std::slice::from_raw_parts(ptr.add(off + 16), 8)
676            }.try_into().unwrap());
677            if rec_h <= target {
678                let vs = off + CHDR + 32;
679                if vs + vlen > cap {
680                    return Err(crate::DbError::Corrupt);
681                }
682                return Ok(unsafe {
683                    std::slice::from_raw_parts(ptr.add(vs), vlen).to_vec()
684                });
685            }
686            if prev == 0 {
687                return Err(crate::DbError::NotFound);
688            }
689            off = prev as usize;
690        }
691    }
692
693    /// Subsystem 5: seal the active log, open a fresh active log, and update
694    /// the manifest and in-memory sealed segment list.
695    ///
696    /// The rename from `seg_active.log` to the new sealed path is a POSIX
697    /// atomic operation; the existing mmap mapping in the `DualBufferEngine`
698    /// remains valid through the rename. The manifest is written twice: once
699    /// with the sealed entry and once with the new active entry, so a crash
700    /// between the two writes leaves a recoverable state.
701    fn rotate(&self, height: u64, actual: u64) -> crate::DbResult<()> {
702        let next_id   = { self.manifest.lock().next_id };
703        let seal_path = self.dir.join(format!("seg_{:04}.log", next_id));
704        let seal_min  = self.manifest.lock().segments.iter()
705            .find(|m| !m.sealed)
706            .map(|m| m.min_height)
707            .unwrap_or(0);
708
709        // Atomically rename the active file; the existing mmap stays valid.
710        std::fs::rename(self.dir.join("seg_active.log"), &seal_path)
711            .map_err(crate::DbError::Io)?;
712
713        // Write the manifest with the sealed entry first, then with the new
714        // active entry. Crashing between the two saves leaves a recoverable
715        // manifest (the new active segment simply won't exist yet and will be
716        // created as empty on the next open).
717        {
718            let mut m = self.manifest.lock();
719            m.segments.retain(|s| s.sealed);
720            m.segments.push(SegmentMeta {
721                id:         next_id,
722                path:       seal_path.to_string_lossy().into(),
723                sealed:     true,
724                used:       actual,
725                min_height: seal_min,
726                max_height: height,
727            });
728            m.next_id += 1;
729            m.save(&self.manifest_path)?;
730            let new_id = m.next_id;
731            m.segments.push(SegmentMeta {
732                id:         new_id,
733                path:       self.dir.join("seg_active.log").to_string_lossy().into(),
734                sealed:     false,
735                used:       0,
736                min_height: height + 1,
737                max_height: u64::MAX,
738            });
739            m.next_id += 1;
740            m.save(&self.manifest_path)?;
741        }
742
743        // Bump the generation counter so that any index entries written to the
744        // old log are recognised as belonging to a sealed segment on the next
745        // get() call.
746        self.gen.fetch_add(1, Ordering::AcqRel);
747        let new_log = Arc::new(CommutativeLog::open(
748            &self.dir.join("seg_active.log"),
749            self.cfg.buffer_size,
750        )?);
751        // Install the new log into shard 0 (updates active/committed aliases)
752        // and into segment_log so get() reads from the new segment immediately.
753        self.dbe.replace_shard_log(0, Arc::clone(&new_log));
754        self.segment_log.store(new_log);
755        self.dbe.vcache.clear();
756
757        // Memory-map the newly sealed segment and append it to the sealed list.
758        let new_meta = self.manifest.lock().segments.iter()
759            .find(|s| s.id == next_id)
760            .ok_or_else(|| crate::DbError::Io(
761                std::io::Error::new(std::io::ErrorKind::NotFound, "sealed meta missing"),
762            ))?
763            .clone();
764        self.sealed.write().push(SealedSegment::open(new_meta)?);
765        Ok(())
766    }
767
768    /// Replay committed records from `log` into `index` and `merkle`.
769    ///
770    /// Called during [`open()`][Self::open] to restore in-memory state from the
771    /// durable active log after a restart or crash. The scan starts at byte
772    /// offset 8 (after the log header) and stops at the first corrupt or
773    /// incomplete record. Only records below the persisted `committed_offset`
774    /// are processed.
775    ///
776    /// For each record, the accumulator is updated by XORing out the old
777    /// `key XOR BLAKE3(old_value)` contribution (if the key was already seen
778    /// earlier in the replay) and XORing in the new `key XOR BLAKE3(value)`.
779    fn replay_into(
780        log:    &Arc<CommutativeLog>,
781        log_id: u64,
782        index:  &Arc<ShardIndex>,
783        merkle: &Arc<parking_lot::Mutex<[u8; 32]>>,
784    ) -> crate::DbResult<()> {
785        let committed = log.committed_offset() as usize;
786        if committed <= 8 {
787            return Ok(());
788        }
789        let ptr = log.mmap_ptr();
790        let mut cur = 8usize;
791        let mut acc = [0u8; 32];
792
793        while cur + CHDR + 32 <= committed {
794            let magic = u32::from_le_bytes(unsafe {
795                std::slice::from_raw_parts(ptr.add(cur), 4)
796            }.try_into().unwrap());
797
798            if magic == CTOMB {
799                let key: [u8; 32] = unsafe {
800                    std::slice::from_raw_parts(ptr.add(cur + CHDR), 32)
801                }.try_into().unwrap();
802                // XOR out using cached value_hash — no mmap re-read needed.
803                if let Some(entry) = index.get_entry(&key) {
804                    if !entry.deleted {
805                        for i in 0..32 { acc[i] ^= key[i] ^ entry.value_hash[i]; }
806                    }
807                }
808                let wc = index.count(&key) + 1;
809                index.upsert_with_deleted(key, cur as u64, wc, log_id, true);
810                cur += CHDR + 32;
811                continue;
812            }
813
814            if magic != CMAG {
815                break;
816            }
817            let vlen = u32::from_le_bytes(unsafe {
818                std::slice::from_raw_parts(ptr.add(cur + 4), 4)
819            }.try_into().unwrap()) as usize;
820            let total = CHDR + 32 + vlen;
821            if cur + total > committed || vlen > committed {
822                break;
823            }
824            let key: [u8; 32] = unsafe {
825                std::slice::from_raw_parts(ptr.add(cur + CHDR), 32)
826            }.try_into().unwrap();
827            let val_end = cur + CHDR + 32 + vlen;
828            if val_end > committed {
829                break;
830            }
831            let val = unsafe { std::slice::from_raw_parts(ptr.add(cur + CHDR + 32), vlen) };
832            let vh  = *blake3::hash(val).as_bytes();
833
834            // Remove the previous contribution of this key from the accumulator.
835            if let Some(entry) = index.get_entry(&key) {
836                if !entry.deleted {
837                    // Use cached value_hash — never re-read from mmap.
838                    for i in 0..32 { acc[i] ^= key[i] ^ entry.value_hash[i]; }
839                }
840            }
841            for i in 0..32 {
842                acc[i] ^= key[i] ^ vh[i];
843            }
844            let wc = index.count(&key) + 1;
845            index.upsert_full(key, cur as u64, wc, log_id, false, vh);
846            cur += total;
847        }
848        log.set_committed(cur as u64);
849        *merkle.lock() = acc;
850        Ok(())
851    }
852}
853
854// ── Subsystem 1: ExecutionFrame ───────────────────────────────────────────────
855
856/// An isolated write arena scoped to a single block.
857///
858/// Writes appended through an `ExecutionFrame` go into the active log immediately
859/// (making them durable on fsync) but are not visible to [`DonaDbX::get()`]
860/// until the frame is committed and the subsequent fold pass updates the index.
861///
862/// Use [`abort()`][ExecutionFrame::abort] to roll back all writes made since the
863/// frame was created, as if they never happened. Use
864/// [`commit()`][ExecutionFrame::commit] to promote them into the committed log.
865pub struct ExecutionFrame<'a> {
866    db:     &'a DonaDbX,
867    /// Write offset in the active log at the time the frame was opened.
868    /// Used as the rollback point on abort.
869    start:  u64,
870    /// Log generation at frame creation. If this differs from the current
871    /// generation at abort time, a rotation has already sealed the frame;
872    /// no rollback is needed.
873    log_id: u64,
874}
875
876impl<'a> ExecutionFrame<'a> {
877    /// Append a key-value pair within this frame.
878    ///
879    /// Identical semantics to [`DonaDbX::put()`]: lock-free, one `fetch_add`,
880    /// one `memcpy`. The write is not reflected in [`DonaDbX::get()`] until
881    /// this frame is committed and the fold completes.
882    #[inline(always)]
883    pub fn put(&self, key: [u8; 32], value: &[u8]) -> crate::DbResult<u64> {
884        self.db.put(key, value)
885    }
886
887    /// Commit all writes in this frame for block `height`.
888    ///
889    /// Equivalent to calling [`DonaDbX::commit()`] directly. The frame is
890    /// consumed and its writes become visible to readers after the fold
891    /// completes.
892    pub fn commit(self, height: u64) -> crate::DbResult<CommitAck> {
893        self.db.commit(height)
894    }
895
896    /// Abort this frame, rolling back all writes made since it was created.
897    ///
898    /// Zero-fills the log region from `start` to the current write offset,
899    /// then resets the write offset to `start`. This makes the region
900    /// available for reuse and leaves no trace in the log.
901    ///
902    /// If a segment rotation occurred after this frame was opened (detected
903    /// by comparing the stored `log_id` with the current generation), the
904    /// frame has already been sealed into a segment. In that case the abort
905    /// is a no-op: the rotation's committed-offset boundary already excludes
906    /// any partial writes from this frame.
907    ///
908    /// The method waits for all in-flight lock-free writes (those that reserved
909    /// space but have not yet finished copying) to complete before zeroing, to
910    /// avoid a data race with concurrent writers in the same shard.
911    pub fn abort(self) {
912        let log_arc = self.db.dbe.active.load_full();
913        let cur_gen = self.db.gen.load(Ordering::Acquire);
914        // If the log was rotated after this frame was created, the frame's
915        // writes are already sealed and there is nothing to roll back.
916        if cur_gen != self.log_id {
917            return;
918        }
919        let _guard = log_arc.commit_lock.lock();
920        // Wait for any concurrent lock-free writers that have reserved space
921        // but have not yet finished their memcpy.
922        while log_arc.inflight.load(Ordering::Acquire) > 0 {
923            std::hint::spin_loop();
924        }
925        let current = log_arc.write_offset() as usize;
926        let start   = self.start as usize;
927        if current > start {
928            unsafe {
929                std::ptr::write_bytes(
930                    log_arc.mmap_ptr().add(start) as *mut u8,
931                    0,
932                    current - start,
933                );
934            }
935        }
936        log_arc.write_offset.store(self.start, Ordering::Release);
937    }
938}
939
940#[cfg(test)]
941mod engine_tests {
942    use super::*;
943    use tempfile::tempdir;
944
945    fn cfg() -> Config { Config { buffer_size: 256 << 20, ..Default::default() } }
946
947    #[test]
948    fn scan_prefix_debug() {
949        let d = tempdir().unwrap();
950        let db = DonaDbX::open(d.path(), cfg()).unwrap();
951        let mut k1 = [0u8; 32]; k1[0] = 0x01; k1[1] = 0x00;
952        let mut k2 = [0u8; 32]; k2[0] = 0x01; k2[1] = 0x01;
953        let mut k3 = [0u8; 32]; k3[0] = 0x02;
954        db.put(k1, b"a").unwrap();
955        db.put(k2, b"b").unwrap();
956        db.put(k3, b"c").unwrap();
957        db.commit(1).unwrap().wait();
958
959        let cur_gen = db.gen.load(Ordering::Acquire);
960        eprintln!("cur_gen = {cur_gen}");
961        for e in db.dbe.index.all_entries() {
962            let rc = db.read_committed(e.offset as usize);
963            let sw = e.key.starts_with(&[0x01u8]);
964            eprintln!("  key[0]={:#04x} log_id={} offset={} deleted={} read_committed={:?} starts_with_01={}",
965                e.key[0], e.log_id, e.offset, e.deleted, rc.as_ref().map(|v| v.len()), sw);
966        }
967        let result = db.scan_prefix(&[0x01u8]).unwrap();
968        eprintln!("scan_prefix results: {}", result.len());
969        assert_eq!(result.len(), 2);
970    }
971
972    #[test]
973    fn cache_populated_and_hit() {
974        let d  = tempdir().unwrap();
975        let db = DonaDbX::open(d.path(), cfg()).unwrap();
976        let mut k = [0u8; 32]; k[0] = 0x42;
977        db.put(k, b"cached_value").unwrap();
978        db.commit(1).unwrap().wait();
979
980        let cache_len = db.dbe.vcache.len();
981        eprintln!("vcache entries after commit: {}", cache_len);
982        assert!(cache_len > 0, "fold thread must populate cache");
983
984        // Direct cache hit
985        let hit = db.dbe.vcache.get(&k);
986        assert!(hit.is_some(), "key must be in cache after fold");
987        assert_eq!(hit.unwrap().as_ref(), b"cached_value");
988
989        // get() must return the cached value
990        assert_eq!(db.get(&k).unwrap(), b"cached_value");
991    }
992
993    #[test]
994    fn optimization_benchmark() {
995        use std::time::Instant;
996        use std::sync::Arc;
997        
998        let d = tempdir().unwrap();
999        let db = Arc::new(DonaDbX::open(d.path(), cfg()).unwrap());
1000        
1001        eprintln!("\n╔════════════════════════════════════════════════════════════╗");
1002        eprintln!("║  DonaDbX Optimization Benchmark                            ║");
1003        eprintln!("╚════════════════════════════════════════════════════════════╝");
1004        
1005        // Test 1: Write throughput
1006        let n = 50_000u64;
1007        let val = vec![42u8; 128];
1008        
1009        eprintln!("\n[1] Write {} keys (128 bytes each)", n);
1010        let write_start = Instant::now();
1011        for i in 0..n {
1012            let mut key = [0u8; 32];
1013            key[..8].copy_from_slice(&i.to_le_bytes());
1014            db.put(key, &val).unwrap();
1015        }
1016        let write_time = write_start.elapsed();
1017        eprintln!("    Write time: {:?} ({:.0} ops/s)", 
1018            write_time, n as f64 / write_time.as_secs_f64());
1019        
1020        // Test 2: Commit + parallel fold
1021        eprintln!("\n[2] Commit + parallel fold");
1022        let commit_start = Instant::now();
1023        let root = db.commit(1).unwrap().wait();
1024        let commit_time = commit_start.elapsed();
1025        eprintln!("    Commit time: {:?}", commit_time);
1026        eprintln!("    State root: {}", hex::encode(&root[..8]));
1027        
1028        // Test 3: Cache effectiveness (CLOCK eviction)
1029        eprintln!("\n[3] Read performance (cache effectiveness)");
1030        let cache_len = db.dbe.vcache.len();
1031        eprintln!("    Cache entries: {}", cache_len);
1032        
1033        let read_start = Instant::now();
1034        let mut hits = 0;
1035        for i in 0..10_000u64 {
1036            let mut key = [0u8; 32];
1037            key[..8].copy_from_slice(&(i * 5).to_le_bytes());
1038            if db.get(&key).is_ok() {
1039                hits += 1;
1040            }
1041        }
1042        let read_time = read_start.elapsed();
1043        eprintln!("    10K reads: {:?} ({:.0} reads/s)", 
1044            read_time, 10_000.0 / read_time.as_secs_f64());
1045        eprintln!("    Hit rate: {:.1}%", hits as f64 / 100.0);
1046        
1047        // Test 4: Multi-shard concurrent writes
1048        eprintln!("\n[4] Concurrent writes (multi-shard)");
1049        let threads = 4;
1050        let per_thread = 10_000u64;
1051        let val_arc = Arc::new(val);
1052        
1053        let concurrent_start = Instant::now();
1054        let handles: Vec<_> = (0..threads).map(|shard_id| {
1055            let db = Arc::clone(&db);
1056            let val = Arc::clone(&val_arc);
1057            std::thread::spawn(move || {
1058                let writer = db.writer(shard_id);
1059                for i in 0..per_thread {
1060                    let mut key = [0u8; 32];
1061                    key[0] = shard_id as u8;
1062                    key[8..16].copy_from_slice(&i.to_le_bytes());
1063                    writer.put(key, &val).unwrap();
1064                }
1065            })
1066        }).collect();
1067        
1068        for h in handles { h.join().unwrap(); }
1069        let concurrent_time = concurrent_start.elapsed();
1070        let total_ops = threads as u64 * per_thread;
1071        eprintln!("    {} threads × {} ops: {:?} ({:.0} ops/s)", 
1072            threads, per_thread, concurrent_time, 
1073            total_ops as f64 / concurrent_time.as_secs_f64());
1074        
1075        let root2 = db.commit(2).unwrap().wait();
1076        eprintln!("    Final index size: {} keys", db.len());
1077        eprintln!("    State root: {}", hex::encode(&root2[..8]));
1078        
1079        eprintln!("\n✓ All optimization benchmarks passed");
1080    }
1081}