11//! Full one-shot compaction (entry point: [`compact_now`]):
22//!
3- //! 1. Collects all live data from main and catalog trees into memory.
4- //! 2. Drains eligible deferred-free pages (now eligible since no pinned reader
5- //! can observe them).
6- //! 3. Writes fresh compacted trees starting at page 4, producing a dense layout.
7- //! 4. Commits a new header with the updated roots and reduced `next_page_id`.
8- //! 5. Truncates main.db if no reader pins the old high-water-mark range.
9- //! 6. Repacks segment files whose garbage ratio exceeds 5%.
3+ //! 1. Refuses while any reader pins the page range (relocation/truncation is
4+ //! unsafe under a pinned reader).
5+ //! 2. Crash-atomically repacks the main + catalog trees into a dense low-address
6+ //! layout and truncates the reclaimed tail (see [`super::repack`]).
7+ //! 3. Repacks segment files whose garbage ratio exceeds 5%.
108
11- use crate :: btree :: BTree ;
9+ use crate :: Result ;
1210use crate :: errors:: PagedbError ;
13- use crate :: pager:: header:: commit_header;
14- use crate :: pager:: structural_header:: MainDbHeaderFields ;
1511use crate :: segment:: reader:: SegmentReader ;
1612use crate :: segment:: types:: SegmentPageKind ;
1713use crate :: segment:: writer:: SegmentWriter ;
1814use crate :: txn:: db:: Db ;
1915use crate :: vfs:: { Vfs , VfsFile } ;
20- use crate :: { CommitId , Result } ;
2116
22- use super :: helpers:: {
23- collect_all_pairs, collect_catalog_split, find_segment_name_inner, list_all_segments_inner,
24- page_size_log2, replace_segment_compact,
25- } ;
17+ use super :: helpers:: { find_segment_name_inner, list_all_segments_inner, replace_segment_compact} ;
2618use super :: types:: CompactStats ;
2719
2820/// Full online compaction. See module-level docs for the staged flow.
@@ -31,7 +23,7 @@ use super::types::CompactStats;
3123/// `EnteredSpan` guard: an entered span guard is `!Send` and would be held
3224/// across the many `.await` points below, making the returned future `!Send`
3325/// and thus uncallable from `Send` async contexts (e.g. the nodedb-lite
34- /// `#[async_trait]` StorageEngine impl, which requires `Send` futures).
26+ /// `#[async_trait]` ` StorageEngine` impl, which requires `Send` futures).
3527pub async fn compact_now < V : Vfs + Clone > ( db : & Db < V > ) -> Result < CompactStats > {
3628 use tracing:: Instrument ;
3729 compact_now_inner ( db)
@@ -55,8 +47,6 @@ async fn compact_now_inner<V: Vfs + Clone>(db: &Db<V>) -> Result<CompactStats> {
5547 // commit can't recycle a page the repack now uses for live data.
5648 db. free_page_cache . lock ( ) . clear ( ) ;
5749
58- let old_next_page_id = state. next_page_id ;
59-
6050 // ── 1. Refuse while readers are pinned ───────────────────────────────────
6151 // A dense repack relocates the current tree and truncates the file; pinned
6252 // readers (in-process or cross-process durable) still reference the old
@@ -81,124 +71,19 @@ async fn compact_now_inner<V: Vfs + Clone>(db: &Db<V>) -> Result<CompactStats> {
8171 return Ok ( result) ;
8272 }
8373
84- // ── 2. Full repack (no readers pinned) ───────────────────────────────────
85-
86- // Collect all live data in memory BEFORE any writes.
87- let main_pairs = if state. root_page_id != 0 {
88- let old_tree = BTree :: open (
89- db. pager . clone ( ) ,
90- db. realm_id ,
91- state. root_page_id ,
92- old_next_page_id,
93- db. page_size ,
94- ) ;
95- collect_all_pairs ( & old_tree) . await ?
96- } else {
97- Vec :: new ( )
98- } ;
99-
100- // Collect catalog rows (housekeeping free-list rows dropped).
101- let cat_rows = collect_catalog_split ( & db. pager , db. realm_id , & state) . await ?;
102-
103- // ── 3. Write fresh compacted trees starting at page 4 ────────────────────
104- // Pages 0–3 are reserved (header slots A/B + two spares); never allocated.
105- let mut new_main = BTree :: open (
106- db. pager . clone ( ) ,
107- db. realm_id ,
108- 0 ,
109- 4 , // first data page (pages 0-3 are reserved header slots)
110- db. page_size ,
111- ) ;
112- new_main. bulk_load ( main_pairs) . await ?;
113- new_main. flush ( ) . await ?;
114- let new_root = new_main. root_page_id ( ) ;
115- let after_main = new_main. next_page_id ( ) ;
116-
117- let mut new_cat = BTree :: open ( db. pager . clone ( ) , db. realm_id , 0 , after_main, db. page_size ) ;
118- new_cat. bulk_load ( cat_rows) . await ?;
119- new_cat. flush ( ) . await ?;
120- let new_cat_root = new_cat. root_page_id ( ) ;
121- let new_next = new_cat. next_page_id ( ) ;
122-
123- // Pages reclaimed = reduction in next_page_id (the dense layout is contiguous,
124- // and the durable free-list is reset to empty below).
125- result. main_db_pages_reclaimed = old_next_page_id. saturating_sub ( new_next) ;
126-
127- // ── 4. Commit new header ─────────────────────────────────────────────────
128- let new_commit_id = state. latest_commit_id + 1 ;
129- let new_seq = state. seq + 1 ;
130- let counter_anchor = db. pager . pending_anchor ( ) ;
131-
132- let mut catalog_root_bytes = [ 0u8 ; 16 ] ;
133- catalog_root_bytes[ ..8 ] . copy_from_slice ( & new_cat_root. to_le_bytes ( ) ) ;
134- catalog_root_bytes[ 8 ..] . copy_from_slice ( & new_commit_id. to_le_bytes ( ) ) ;
135-
136- let fields = MainDbHeaderFields {
137- format_version : 1 ,
138- cipher_id : db. cipher_id . as_byte ( ) ,
139- page_size_log2 : page_size_log2 ( db. page_size ) ?,
140- flags : 0 ,
141- file_id : db. file_id ,
142- kek_salt : db. kek_salt ,
143- mk_epoch : db. mk_epoch . load ( std:: sync:: atomic:: Ordering :: SeqCst ) ,
144- seq : new_seq,
145- active_root_page_id : new_root,
146- active_root_txn_id : new_commit_id,
147- counter_anchor,
148- commit_id : CommitId ( new_commit_id) ,
149- free_list_root : [ 0u8 ; 16 ] ,
150- catalog_root : catalog_root_bytes,
151- apply_journal_root_page_id : 0 ,
152- apply_journal_root_version : 0 ,
153- commit_history_root_page_id : 0 ,
154- commit_history_root_version : 0 ,
155- restore_mode : 0 ,
156- next_page_id : new_next,
157- commit_retain_policy_tag : 0 ,
158- commit_retain_policy_value : 0 ,
159- } ;
160-
161- let hk_clone = { db. hk . read ( ) . clone ( ) } ;
162- let new_slot = commit_header (
163- & * db. vfs ,
164- & db. main_db_path ,
165- & hk_clone,
166- & fields,
167- state. active_slot ,
168- db. page_size ,
169- )
170- . await ?;
171- db. pager . commit_anchor ( counter_anchor) ?;
172-
173- state. root_page_id = new_root;
174- state. catalog_root_page_id = new_cat_root;
175- state. next_page_id = new_next;
176- state. active_slot = new_slot;
177- state. seq = new_seq;
178- state. latest_commit_id = new_commit_id;
179- state. commit_history_root_page_id = 0 ;
180- state. commit_history_root_version = 0 ;
181- // The dense repack relocates/truncates every page, so the old free-list is
182- // gone; the new layout starts with an empty free-list.
183- state. free_list_root_page_id = 0 ;
184- db. latest_commit
185- . store ( new_commit_id, std:: sync:: atomic:: Ordering :: SeqCst ) ;
186-
187- // ── 5. Truncate if no readers pin the old high-water range ───────────────
188- // (No readers are pinned at this point — checked above — so truncation is safe.)
189- if new_next < old_next_page_id {
190- let new_size = new_next. saturating_mul ( db. page_size as u64 ) ;
191- let old_size = old_next_page_id. saturating_mul ( db. page_size as u64 ) ;
192- let mut f = db
193- . vfs
194- . open ( & db. main_db_path , crate :: vfs:: types:: OpenMode :: ReadWrite )
195- . await ?;
196- f. set_len ( new_size) . await ?;
197- f. sync ( ) . await ?;
198- result. bytes_truncated = old_size. saturating_sub ( new_size) ;
74+ // ── 2. Crash-atomic dense repack of the main + catalog trees ─────────────
75+ // An empty free-list means every page below the high-water mark is live —
76+ // the store is already dense, so there is nothing to reclaim and we skip the
77+ // repack entirely (no wasted rewrite). Otherwise repack via a scratch file +
78+ // atomic rename; main.db is never modified until the rename (see
79+ // `super::repack`).
80+ if state. free_list_root_page_id != 0 {
81+ let repack = super :: repack:: atomic_dense_repack ( db, & mut state) . await ?;
82+ result. main_db_pages_reclaimed = repack. pages_reclaimed ;
83+ result. bytes_truncated = repack. bytes_truncated ;
19984 }
20085
201- // ── 6 . Repack segments ────────────────────────────────────────────────────
86+ // ── 3 . Repack segments ────────────────────────────────────────────────────
20287 let all_segments = list_all_segments_inner ( & db. pager , db. realm_id , & state) . await ?;
20388 for meta in all_segments {
20489 let live = crate :: segment:: writer:: live_path ( & meta. segment_id ) ;
0 commit comments