Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
751 changes: 488 additions & 263 deletions src/transaction/freemap.rs

Large diffs are not rendered by default.

65 changes: 16 additions & 49 deletions src/transaction/lifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,24 +88,12 @@ impl TransactionManager {
}
self.current_roots = self.committed_roots.clone();
// The freemap root+depth ride in current_roots (cloned just above), so
// there is no separate freemap working copy to reset here. The hint is
// untracked (a stale hint only costs a scan), so it is left as-is too.
// The session-owned set is strictly per-transaction: a page COW'd last
// transaction is now committed and must NOT be mutated in place, so start
// empty. (begin already requires no active txn, so it is normally empty,
// but clear defensively.)
self.freemap_session_owned.clear();
// Seed the structural reuse pool from the prior commit's deferred dead
// freemap pages: those superblock-unreferenced pages are now safe to
// reuse as this transaction's freemap COW targets, so the freemap rotates
// among a bounded set instead of extending. CLONE (not move) so
// `pending_structural_frees` stays intact as the rollback fallback — a
// rolled-back transaction never reached commit, so its structural recycle
// is exactly the pre-transaction one. `commit_inner` overwrites it on the
// success path. `structural_superseded` is empty here (only
// persist_freemap fills it); clear defensively.
self.structural_reuse = self.pending_structural_frees.clone();
self.structural_superseded.clear();
// there is no separate freemap working copy to reset here. Reset the
// recycle's per-transaction state: clear the session set and reseed the
// structural reuse pool from the prior commit's deferred dead freemap
// pages (the hint persists across transactions — a stale hint only costs a
// scan). See `FreemapRecycle::begin`.
self.freemap.begin();
// R1: clone the live-slot counts and reset the insert cursor.
// The cursor is always None at begin — it only tracks pages
// allocated during the current transaction.
Expand Down Expand Up @@ -337,19 +325,11 @@ impl TransactionManager {
// txn_freed_pages were already marked free in the new committed freemap
// tree by persist_freemap; clear the vector now that it's done its job.
self.txn_freed_pages.clear();
// Every freemap page COW'd this transaction is now committed; the next
// transaction must COW (not edit in place) any of them it touches.
self.freemap_session_owned.clear();
// Promote the freemap structural recycle for the next transaction: the
// pages this commit superseded (`structural_superseded`) become dead the
// instant the superblock flips above — and the reuse-pool remainder
// (`structural_reuse` ids not consumed as COW targets) is likewise still
// dead and reusable. Both become next transaction's `pending_structural_frees`.
self.pending_structural_frees.clear();
self.pending_structural_frees
.append(&mut self.structural_superseded);
self.pending_structural_frees
.append(&mut self.structural_reuse);
// Clear the freemap session set (every page COW'd this transaction is now
// committed) and promote the structural recycle for the next transaction:
// this commit's supersedes + the unconsumed reuse remainder become next
// transaction's `pending_structural_frees`. See `FreemapRecycle::commit`.
self.freemap.commit();

Ok(())
}
Expand Down Expand Up @@ -426,24 +406,11 @@ impl TransactionManager {
// The freemap root+depth were restored by `current_roots =
// committed_roots.clone()` above; any dirty freemap pages this
// transaction COW'd sit above the watermark and were dropped by the
// truncate. The hint is untracked, so nothing to revert.
//
// `pending_structural_frees` is left intact: begin() CLONED it into
// `structural_reuse` rather than moving it, so it still holds the
// pre-transaction dead-freemap-page set — correct, since a rolled-back
// transaction's structural recycle is exactly the pre-transaction one.
// We DISCARD the in-transaction structural working state:
// * `structural_superseded` holds committed-tree freemap pages this
// aborted transaction COW'd-over; the abort means the committed tree
// still references them, so they are NOT dead and must never be
// recycled.
// * `structural_reuse` was the working copy; drop it.
// * the session-owned set: any freemap pages this aborted transaction
// COW'd sit above the watermark and were just truncated, so their ids
// must not be treated as in-place-mutable next transaction.
self.structural_superseded.clear();
self.structural_reuse.clear();
self.freemap_session_owned.clear();
// truncate. Discard the in-transaction structural working state
// (`structural_superseded` + `structural_reuse` + the session set) while
// leaving `pending_structural_frees` intact as the pre-transaction
// baseline. See `FreemapRecycle::rollback` for the per-stream reasoning.
self.freemap.rollback();
// R1: revert the live-slot counts and drop the insert cursor.
self.packer.rollback();
self.active_txn = false;
Expand Down
68 changes: 8 additions & 60 deletions src/transaction/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,66 +184,14 @@ pub struct TransactionManager {
// their old contents must stay readable via `committed_roots` until
// commit promotes the new roots.
txn_freed_pages: Vec<u64>,
// Best-effort lower bound on the lowest free page id in the committed
// freemap tree, threaded into `FreeMapTree::allocate_first` so a scan
// starts near the answer instead of at id 0. Deliberately NOT
// transactionally tracked: a too-low hint only costs a wasted left-to-right
// scan, never correctness (the scan still returns the true lowest free id),
// so it needs no begin/rollback snapshotting. `allocate_first` advances it;
// a free at a lower id is invisible to the hint until the next scan walks
// back over it, which is acceptable slack. Init 0.
freemap_hint: u64,
// Dead freemap pages carried BETWEEN commits, the engine's bounded-growth
// mechanism for the extend-only freemap (ISSUES.md I18, generalized to the
// tree). Lifecycle:
//
// * A freemap mutation (data-alloc-side leaf COW, or persist's frees) must
// COW the committed freemap pages it touches — it can never overwrite a
// page the last-durable superblock still references. Each COW supersedes
// an OLD freemap page.
// * That old page cannot be reused IN THE SAME COMMIT (the commit's new
// freemap root may still reference it until the superblock flips), so it
// is DEFERRED one commit: collected in `structural_superseded` this
// transaction, promoted to `pending_structural_frees` at commit.
// * The NEXT transaction reuses them: `begin()` moves them into
// `structural_reuse`, and every structural `extend` (freemap COW target)
// pops from that pool before extending the file. This is what makes the
// freemap leaf ROTATE among a small set of pages instead of marching the
// file upward ~1 page/commit forever. Reusing a DEAD page (vs. a free bit
// in the tree) keeps the extend-only TERMINATION guarantee — no freemap
// mutation ever draws structural space from the freemap's own bits.
//
// Not data-reusable (never enters `txn_freed_pages`): a freed freemap page
// sits at a high id, and the lowest-first data allocator would starve it, so
// routing it back as structural reuse (where demand matches supply at steady
// state) is what actually reclaims it.
pending_structural_frees: Vec<u64>,
// The dead-freemap-page pool available to reuse as structural COW targets in
// the CURRENT transaction. Seeded from `pending_structural_frees` at
// `begin()`; drained by every structural `extend`; the unconsumed remainder
// is carried forward (back into `pending_structural_frees`) at commit. On
// rollback it is moved back wholesale, restoring the pre-transaction
// `pending_structural_frees`.
structural_reuse: Vec<u64>,
// This transaction's freemap-COW supersedes (old freemap pages this txn
// replaced). Accumulated as transient handles drain `tree.pending_superseded`
// here via `put_freemap_tree`; promoted to `pending_structural_frees` at
// commit (the one-commit defer). Dropped on rollback (those COWs are
// truncated above the watermark).
structural_superseded: Vec<u64>,
// Freemap pages already COW'd/extended by the CURRENT transaction. Because
// the manager rebuilds a transient `FreeMapTree` handle at every allocation
// site (data-page alloc, each HT/membership COW, persist_freemap), this set
// is what lets those handles share the "first touch this txn => COW, later
// touches => in-place" discipline: without it every site would re-COW the
// same freemap leaf, turning reclamation into unbounded file growth. Swapped
// into each transient handle and read back out (see `freemap_tree` helper).
// Cleared at begin (fresh per transaction); also cleared on commit/rollback
// so the next transaction starts empty. A stale entry pointing at a
// now-committed page would be a CORRECTNESS bug (it would suppress a needed
// COW and mutate a live committed page in place), which is exactly why it is
// transaction-scoped, not cross-transaction.
freemap_session_owned: FxHashSet<u64>,
// The structural-page recycle cluster and freemap commit/alloc/persist
// machinery — the crash-durability backbone — extracted into its own owned
// unit. No code outside `freemap.rs` touches the inner fields; the rest of
// `TransactionManager` reaches the freemap through `FreemapRecycle`'s narrow
// surface (the transient-tree trio, the commit-path persist/reclaim wrappers
// on this type, and the begin/commit/rollback lifecycle hooks). See the
// `FreemapRecycle` doc in `freemap.rs` for the recycle/rotation model.
freemap: freemap::FreemapRecycle,
// R1 slot-packing state (live-slot counts per data page + the insert
// cursor), extracted into its own owned unit. No code outside `packing.rs`
// touches the inner fields; `TransactionManager` reaches packing through
Expand Down
9 changes: 3 additions & 6 deletions src/transaction/mutate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
//! delete_with_tag / delete_many (+ their `_inner` cores). Split out of
//! `transaction.rs` verbatim; see the parent module for the type and fields.

use super::freemap::cow_alloc;
use super::*;

impl TransactionManager {
Expand Down Expand Up @@ -193,12 +192,10 @@ impl TransactionManager {
// InvalidHandle to preserve the public-API behavior.
let mut ht_freed: Vec<u64> = Vec::new();
let reuse = self.savepoints.is_empty();
let mut tree = self.take_freemap_tree();
let mut tree = self.freemap.take_tree(&self.current_roots);
let delete_result = {
let hint = &mut self.freemap_hint;
let pool = &mut self.structural_reuse;
let mut cache = self.cache.borrow_mut();
let mut alloc = |c: &mut PageCache| cow_alloc(c, &mut tree, hint, pool, reuse);
let mut alloc = |c: &mut PageCache| self.freemap.cow_alloc_into(c, &mut tree, reuse);
self.handle_table.delete(
&mut cache,
self.current_roots.handle_table_page,
Expand All @@ -210,7 +207,7 @@ impl TransactionManager {
// Install freemap growth (supersedes go to structural_superseded). Done
// BEFORE the `?` so a delete that COW'd the freemap leaf yet then errored
// still records the extended root and returns the session set.
self.put_freemap_tree(tree);
self.freemap.put_tree(&mut self.current_roots, tree);
let (ht_new_root, prev_entry) = delete_result?;
let entry = prev_entry.ok_or(ChiselError::InvalidHandle(handle))?;

Expand Down
26 changes: 10 additions & 16 deletions src/transaction/packing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -260,17 +260,14 @@ impl SlotPacker {
impl TransactionManager {
/// Place a value in a data page and return (page_id, slot_index).
///
/// Thin wrapper over `SlotPacker::insert`. The freemap dance that
/// `allocate_data_page` performs (take/put the transient `FreeMapTree`,
/// touching `current_roots`, `freemap_session_owned`,
/// `structural_superseded`, `freemap_hint`, `structural_reuse`) is hoisted
/// HERE, around the closure, exactly like `ht_insert`: those are
/// `&mut self` operations and cannot run inside a closure that the packer
/// borrow (`&mut self.packer`) would also need. The closure captures only
/// the disjoint freemap field refs plus the local `tree`, and takes `cache`
/// as a param.
/// Thin wrapper over `SlotPacker::insert`. The freemap dance (the
/// transient-tree trio: `freemap.take_tree` / `cow_alloc_into` / `put_tree`)
/// is hoisted HERE, around the closure, exactly like `ht_insert`. The closure
/// captures only `&mut self.freemap` plus the local `tree` — disjoint from
/// `&mut self.packer` — so the packer borrow and the alloc closure both hold
/// `self` at once; `cache` is passed as a param.
///
/// `put_freemap_tree` runs even on the error path — matching the historical
/// `put_tree` runs even on the error path — matching the historical
/// `allocate_data_page`, which wrote tree growth back before returning the
/// allocation result. The freemap pages were extended (never freed), so on a
/// non-fatal failure they are harmless above-watermark scratch, and the
Expand All @@ -284,17 +281,14 @@ impl TransactionManager {
// named separately.
let reuse = self.savepoints.is_empty();
let packing_enabled = self.savepoints.is_empty();
let mut tree = self.take_freemap_tree();
let mut tree = self.freemap.take_tree(&self.current_roots);
let result = {
let hint = &mut self.freemap_hint;
let pool = &mut self.structural_reuse;
let mut cache = self.cache.borrow_mut();
let mut alloc =
|c: &mut PageCache| super::freemap::cow_alloc(c, &mut tree, hint, pool, reuse);
let mut alloc = |c: &mut PageCache| self.freemap.cow_alloc_into(c, &mut tree, reuse);
self.packer
.insert(&mut cache, &mut alloc, packing_enabled, value)
};
self.put_freemap_tree(tree);
self.freemap.put_tree(&mut self.current_roots, tree);
result
}

Expand Down
12 changes: 2 additions & 10 deletions src/transaction/recovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,7 @@ impl TransactionManager {
active_txn: false,
savepoints: Vec::new(),
txn_freed_pages: Vec::new(),
freemap_hint: 0,
pending_structural_frees: Vec::new(),
structural_reuse: Vec::new(),
structural_superseded: Vec::new(),
freemap_session_owned: FxHashSet::default(),
freemap: freemap::FreemapRecycle::new(),
// A fresh database has no data pages and no live slots yet.
packer: packing::SlotPacker::new(),
poisoned: Cell::new(false),
Expand Down Expand Up @@ -314,11 +310,7 @@ impl TransactionManager {
active_txn: false,
savepoints: Vec::new(),
txn_freed_pages: Vec::new(),
freemap_hint: 0,
pending_structural_frees: Vec::new(),
structural_reuse: Vec::new(),
structural_superseded: Vec::new(),
freemap_session_owned: FxHashSet::default(),
freemap: freemap::FreemapRecycle::new(),
packer: packing::SlotPacker::from_committed(committed_live_slots),
poisoned: Cell::new(false),
#[cfg(test)]
Expand Down
29 changes: 11 additions & 18 deletions src/transaction/staging.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
//! roots, the abort-prepare unwind, and the membership-failure injection
//! hook. Split out of `transaction.rs` verbatim; see the parent module.

use super::freemap::cow_alloc;
use super::*;

impl TransactionManager {
Expand Down Expand Up @@ -45,12 +44,10 @@ impl TransactionManager {
freed: &mut Vec<u64>,
) -> Result<u64> {
let reuse = self.savepoints.is_empty();
let mut tree = self.take_freemap_tree();
let mut tree = self.freemap.take_tree(&self.current_roots);
let result = {
let hint = &mut self.freemap_hint;
let pool = &mut self.structural_reuse;
let mut cache = self.cache.borrow_mut();
let mut alloc = |c: &mut PageCache| cow_alloc(c, &mut tree, hint, pool, reuse);
let mut alloc = |c: &mut PageCache| self.freemap.cow_alloc_into(c, &mut tree, reuse);
self.membership_index.insert(
&mut cache,
self.current_roots.membership_index_page,
Expand All @@ -64,10 +61,10 @@ impl TransactionManager {
// pages were extended (never freed), so a non-fatal failure that discards
// `freed`/the candidate root leaves these extra pages as harmless
// above-watermark scratch, exactly like the other COW pages on an aborted
// prepare. put_freemap_tree drains the freemap COW supersedes into
// prepare. put_tree drains the freemap COW supersedes into
// structural_superseded and returns the session set so the next site in
// this transaction stays in-place.
self.put_freemap_tree(tree);
self.freemap.put_tree(&mut self.current_roots, tree);
result
}

Expand All @@ -94,12 +91,10 @@ impl TransactionManager {
return Err(ChiselError::CacheFull { limit: 0 });
}
let reuse = self.savepoints.is_empty();
let mut tree = self.take_freemap_tree();
let mut tree = self.freemap.take_tree(&self.current_roots);
let result = {
let hint = &mut self.freemap_hint;
let pool = &mut self.structural_reuse;
let mut cache = self.cache.borrow_mut();
let mut alloc = |c: &mut PageCache| cow_alloc(c, &mut tree, hint, pool, reuse);
let mut alloc = |c: &mut PageCache| self.freemap.cow_alloc_into(c, &mut tree, reuse);
self.handle_table.insert(
&mut cache,
self.current_roots.handle_table_page,
Expand All @@ -112,9 +107,9 @@ impl TransactionManager {
// Install freemap growth into roots (and return the session set) so the
// NEXT candidate in this allocate (the reverse-map insert) threads the
// up-to-date tree and treats already-COW'd freemap pages as in-place. The
// freemap COW supersedes go to structural_superseded via put_freemap_tree.
// freemap COW supersedes go to structural_superseded via put_tree.
// See membership_insert_candidate for the abort-safety reasoning.
self.put_freemap_tree(tree);
self.freemap.put_tree(&mut self.current_roots, tree);
result
}

Expand Down Expand Up @@ -171,12 +166,10 @@ impl TransactionManager {
freed: &mut Vec<u64>,
) -> Result<(u64, bool)> {
let reuse = self.savepoints.is_empty();
let mut tree = self.take_freemap_tree();
let mut tree = self.freemap.take_tree(&self.current_roots);
let result = {
let hint = &mut self.freemap_hint;
let pool = &mut self.structural_reuse;
let mut cache = self.cache.borrow_mut();
let mut alloc = |c: &mut PageCache| cow_alloc(c, &mut tree, hint, pool, reuse);
let mut alloc = |c: &mut PageCache| self.freemap.cow_alloc_into(c, &mut tree, reuse);
self.membership_index.remove(
&mut cache,
self.current_roots.membership_index_page,
Expand All @@ -186,7 +179,7 @@ impl TransactionManager {
freed,
)
};
self.put_freemap_tree(tree);
self.freemap.put_tree(&mut self.current_roots, tree);
result
}

Expand Down
Loading
Loading