|
| 1 | +//! Where a write session's pages come from, supplied to a tree as one value. |
| 2 | +//! |
| 3 | +//! Reuse eligibility is decided when a page is freed, not when one is |
| 4 | +//! allocated (see [`BTree::free_page`](super::core::BTree::free_page)). That is |
| 5 | +//! only sound while every input to the decision is already in place at the |
| 6 | +//! first free and cannot move afterwards, so the inputs travel together in |
| 7 | +//! [`PageSource`] and are handed over once, at |
| 8 | +//! [`BTree::open_session`](super::core::BTree::open_session). There is no |
| 9 | +//! setter for any of them: a tree either has a session's page source for its |
| 10 | +//! whole life or has none and bump-allocates. |
| 11 | +//! |
| 12 | +//! [`ConsumedPages`] carries the other half of that guarantee. A page recorded |
| 13 | +//! there is eligible for in-session reuse, and pages already banked on the |
| 14 | +//! strength of that record cannot be un-banked — so the sink must not shrink |
| 15 | +//! while a session is running. Trees therefore hold a [`ConsumedHandle`], which |
| 16 | +//! can record and test but cannot empty; emptying is [`ConsumedPages::take`], |
| 17 | +//! reachable only from whoever owns the sink, at a transaction boundary. |
| 18 | +
|
| 19 | +use std::sync::Arc; |
| 20 | + |
| 21 | +use rustc_hash::FxHashSet; |
| 22 | + |
| 23 | +/// Page ids the allocator drew from the shared free-page cache and reused this |
| 24 | +/// session. The commit path removes them from the durable free-list — they now |
| 25 | +/// hold live committed data — and [`BTree::free_page`] consults them: a page |
| 26 | +/// drawn from the cache is free as of the last durable header regardless of |
| 27 | +/// what this session wrote to it, so it may be recycled again in-session even |
| 28 | +/// from below the reuse threshold. |
| 29 | +/// |
| 30 | +/// A set, not a list: membership is tested once per freed page, and the size is |
| 31 | +/// bounded by the free-list window (`WINDOW_PAGES × chain_capacity(page_size)`) |
| 32 | +/// rather than by anything small — a linear test would keep the per-free cost |
| 33 | +/// proportional to that window. Nothing reads insertion order or needs |
| 34 | +/// duplicates. |
| 35 | +/// |
| 36 | +/// [`BTree::free_page`]: super::core::BTree::free_page |
| 37 | +#[derive(Debug, Default)] |
| 38 | +pub struct ConsumedPages(parking_lot::Mutex<FxHashSet<u64>>); |
| 39 | + |
| 40 | +impl ConsumedPages { |
| 41 | + /// An empty sink, ready to be shared with a session's trees via |
| 42 | + /// [`Self::handle`]. |
| 43 | + #[must_use] |
| 44 | + pub fn new() -> Arc<Self> { |
| 45 | + Arc::new(Self::default()) |
| 46 | + } |
| 47 | + |
| 48 | + /// A tree's view of this sink: record and test, never empty. Handing trees |
| 49 | + /// this instead of the `Arc` is what makes the sink's monotonicity within a |
| 50 | + /// session a property of the types rather than of caller discipline. |
| 51 | + #[must_use] |
| 52 | + pub fn handle(self: &Arc<Self>) -> ConsumedHandle { |
| 53 | + ConsumedHandle(Arc::clone(self)) |
| 54 | + } |
| 55 | + |
| 56 | + /// Empty the sink and return what it held. |
| 57 | + /// |
| 58 | + /// A transaction-boundary operation, and the only way to empty it. Calling |
| 59 | + /// this while a session's trees are still allocating would strip the record |
| 60 | + /// that made already-banked pages eligible, leaving them banked on a reason |
| 61 | + /// that no longer exists — which is why no [`ConsumedHandle`] can reach it. |
| 62 | + pub fn take(&self) -> FxHashSet<u64> { |
| 63 | + std::mem::take(&mut *self.0.lock()) |
| 64 | + } |
| 65 | +} |
| 66 | + |
| 67 | +/// A tree's handle on a session's [`ConsumedPages`]. Clone-shared across the |
| 68 | +/// trees of one transaction. |
| 69 | +#[derive(Clone, Debug)] |
| 70 | +pub struct ConsumedHandle(Arc<ConsumedPages>); |
| 71 | + |
| 72 | +impl ConsumedHandle { |
| 73 | + pub(crate) fn record(&self, page_id: u64) { |
| 74 | + self.0.0.lock().insert(page_id); |
| 75 | + } |
| 76 | + |
| 77 | + pub(crate) fn contains(&self, page_id: u64) -> bool { |
| 78 | + self.0.0.lock().contains(&page_id) |
| 79 | + } |
| 80 | +} |
| 81 | + |
| 82 | +/// The pages a write session may allocate from, beyond bumping its own cursor. |
| 83 | +/// |
| 84 | +/// Constructed once per session and cloned to each of its trees, so a tree can |
| 85 | +/// never observe a partly-wired source — a cache without the sink that records |
| 86 | +/// draws from it, or a threshold arriving after pages have already been |
| 87 | +/// classified against the default. |
| 88 | +#[derive(Clone, Debug)] |
| 89 | +pub struct PageSource { |
| 90 | + /// Minimum `page_id` that may be recycled from within this session. Pages |
| 91 | + /// below it existed before the session began: they are still referenced by |
| 92 | + /// the last durable header (a copy-on-write free does not unreference them |
| 93 | + /// on disk until the *next* header lands) and may also be pinned by reader |
| 94 | + /// snapshots, so their bytes must survive until the header that frees them |
| 95 | + /// is durable. They re-enter circulation through the deferred-free queue. |
| 96 | + /// |
| 97 | + /// Callers pass the session-start `next_page_id`, so only pages |
| 98 | + /// bump-allocated within the session are recyclable immediately. Zero means |
| 99 | + /// every freed page is (the commit-history tree, whose pages no reader |
| 100 | + /// snapshot names). |
| 101 | + pub(crate) reuse_threshold: u64, |
| 102 | + /// Cross-commit pool of reusable page ids, shared across the main, catalog |
| 103 | + /// and history trees of one `Db`. Loaded at `begin_write` with *only* |
| 104 | + /// free-list pages below the reclamation floor — pages no live reader and |
| 105 | + /// no retained-history root can observe — so drawing from it is safe |
| 106 | + /// regardless of `reuse_threshold`. |
| 107 | + /// |
| 108 | + /// Draw-only: nothing may push into it. The commit deletes a recycled id |
| 109 | + /// from the chain by finding it in the scanned window, so an id pushed in |
| 110 | + /// from elsewhere would be handed out while the unscanned tail still named |
| 111 | + /// it — the same page id given to two live structures. |
| 112 | + pub(crate) cache: Arc<parking_lot::Mutex<Vec<u64>>>, |
| 113 | + /// Where draws from `cache` are recorded. |
| 114 | + pub(crate) consumed: ConsumedHandle, |
| 115 | +} |
| 116 | + |
| 117 | +impl PageSource { |
| 118 | + /// Bind a session's allocation state to one value. `reuse_threshold` is the |
| 119 | + /// session-start `next_page_id` for trees a reader may hold a snapshot of, |
| 120 | + /// and zero for those none can. |
| 121 | + #[must_use] |
| 122 | + pub fn new( |
| 123 | + reuse_threshold: u64, |
| 124 | + cache: Arc<parking_lot::Mutex<Vec<u64>>>, |
| 125 | + consumed: &Arc<ConsumedPages>, |
| 126 | + ) -> Self { |
| 127 | + Self { |
| 128 | + reuse_threshold, |
| 129 | + cache, |
| 130 | + consumed: consumed.handle(), |
| 131 | + } |
| 132 | + } |
| 133 | +} |
0 commit comments