@@ -52,14 +52,18 @@ pub struct BTree<V: Vfs> {
5252 /// parallel-AEAD flush. In-place mutation by subsequent puts targeting
5353 /// the same leaf is allowed; no further allocation needed.
5454 pub ( super ) fresh_leaves : HashMap < u64 , Leaf > ,
55- /// Cross-commit pool of pre-vetted reusable page IDs, shared (via `Arc`)
56- /// across all `BTree`s opened by the same `Db`. Allocation pops from
57- /// here before bumping `next_page_id`, keeping the file size bounded
58- /// when `OpenOptions::skip_freelist_persistence_when_no_readers` orphans
59- /// would otherwise accumulate . `None` for callers that haven't wired a
60- /// shared cache (compaction, history tree) — those keep bump-only
61- /// allocation.
55+ /// Cross-commit pool of reusable page IDs, shared (via `Arc`) across the
56+ /// main, catalog, and history `BTree`s of one `Db`. Allocation pops from
57+ /// here before bumping `next_page_id`, recycling pages freed by earlier
58+ /// commits (once no reader or retained-history root can observe them) so
59+ /// the file stays bounded under sustained writes . `None` for callers that
60+ /// haven't wired a shared cache (e.g. compaction's repack trees), which
61+ /// keep bump-only allocation.
6262 pub ( super ) free_page_cache : Option < Arc < parking_lot:: Mutex < Vec < u64 > > > > ,
63+ /// Sink recording page ids drawn from `free_page_cache` and reused this
64+ /// session. The commit path removes them from the durable free-list (they
65+ /// now hold live committed data). Shared (via `Arc`) across the txn's trees.
66+ pub ( super ) free_page_consumed : Option < Arc < parking_lot:: Mutex < Vec < u64 > > > > ,
6367 /// Last key successfully appended via [`Self::put_append`]. Used to
6468 /// enforce the monotonic-key invariant on subsequent calls and to
6569 /// invalidate the cached path when any non-append mutation (regular
@@ -96,6 +100,7 @@ impl<V: Vfs> BTree<V> {
96100 dirty_parent_paths : HashMap :: new ( ) ,
97101 fresh_leaves : HashMap :: new ( ) ,
98102 free_page_cache : None ,
103+ free_page_consumed : None ,
99104 append_last_key : None ,
100105 append_cached_path : None ,
101106 }
@@ -110,22 +115,18 @@ impl<V: Vfs> BTree<V> {
110115 }
111116
112117 /// Wire in the `Db`'s shared free-page cache. After this call,
113- /// `allocate_page` will pop from the shared pool before bumping
114- /// `next_page_id`. Pages pushed into the pool by an earlier writer
115- /// commit (via [`Self::push_to_shared_cache`]) become reusable here .
118+ /// `allocate_page` pops from the shared pool before bumping `next_page_id`.
119+ /// The pool is loaded at `begin_write` with the durable free-list's
120+ /// floor-safe pages, so recycling from it is always snapshot-safe .
116121 pub fn set_free_page_cache ( & mut self , cache : Arc < parking_lot:: Mutex < Vec < u64 > > > ) {
117122 self . free_page_cache = Some ( cache) ;
118123 }
119124
120- /// Push `page_ids` into the shared free-page cache, if one is wired.
121- /// Used by the writer commit path when the no-reader fast-free option
122- /// is active: instead of orphaning freed pages, hand them to the next
123- /// txn's allocator.
124- pub fn push_to_shared_cache ( & self , page_ids : & [ u64 ] ) {
125- if let Some ( cache) = & self . free_page_cache {
126- let mut guard = cache. lock ( ) ;
127- guard. extend ( page_ids. iter ( ) . copied ( ) ) ;
128- }
125+ /// Wire the shared sink that records cache pages reused this session, so the
126+ /// commit path can remove them from the durable free-list. Set alongside
127+ /// [`Self::set_free_page_cache`].
128+ pub fn set_free_page_consumed ( & mut self , consumed : Arc < parking_lot:: Mutex < Vec < u64 > > > ) {
129+ self . free_page_consumed = Some ( consumed) ;
129130 }
130131
131132 #[ must_use]
@@ -148,34 +149,31 @@ impl<V: Vfs> BTree<V> {
148149 }
149150
150151 pub ( super ) fn allocate_page ( & mut self ) -> u64 {
151- // Reuse a freed page only if it is at or above the reuse threshold.
152- // Pages below the threshold may still be live in pinned reader snapshots.
152+ // First, recycle a page freed earlier *in this same session*, gated by
153+ // the reuse threshold: a page below it may still be live in a pinned
154+ // reader's snapshot, so it can't be reused until the durable free-list
155+ // clears it (it leaves via `drain_freed` at commit instead).
153156 if self . reuse_threshold == 0 {
154- // No readers pinned: recycle freely.
155157 if let Some ( id) = self . freed . pop ( ) {
156158 return id;
157159 }
158- // Consult the cross-commit shared cache (pages handed off by
159- // earlier writer commits under the no-reader fast-free option).
160- // Only safe to draw from this when no readers are pinned — the
161- // cache contract is "always safe to immediately reuse."
162- if let Some ( cache) = & self . free_page_cache {
163- if let Some ( id) = cache. lock ( ) . pop ( ) {
164- return id;
160+ } else if let Some ( pos) = self
161+ . freed
162+ . iter ( )
163+ . rposition ( |& id| id >= self . reuse_threshold )
164+ {
165+ return self . freed . remove ( pos) ;
166+ }
167+ // Then draw from the shared cross-commit cache. It is loaded at txn
168+ // begin with *only* free-list pages below the reclamation floor — pages
169+ // no live reader and no retained-history root can observe — so reusing
170+ // them is safe regardless of `reuse_threshold`. Record each draw so the
171+ // commit path removes it from the durable free-list.
172+ if let Some ( cache) = & self . free_page_cache {
173+ if let Some ( id) = cache. lock ( ) . pop ( ) {
174+ if let Some ( consumed) = & self . free_page_consumed {
175+ consumed. lock ( ) . push ( id) ;
165176 }
166- }
167- } else {
168- // Readers pinned: only recycle pages that were allocated during
169- // this session (>= reuse_threshold) and thus cannot be in any
170- // prior snapshot. The shared cache is bypassed here because
171- // pages in it predate this session and may be visible to a
172- // pinned reader at an older commit.
173- if let Some ( pos) = self
174- . freed
175- . iter ( )
176- . rposition ( |& id| id >= self . reuse_threshold )
177- {
178- let id = self . freed . remove ( pos) ;
179177 return id;
180178 }
181179 }
0 commit comments