@@ -47,18 +47,18 @@ async fn compact_now_no_free_pages() {
4747 let _ = stats; // stats are informational; just verify it completes
4848}
4949
50- // ─── Test 3: compact_step advances watermark in each call ─────────── ─────────
50+ // ─── Test 3: compact_step reclaims freed space and reports completion ─────────
5151
5252#[ tokio:: test( flavor = "current_thread" ) ]
53- async fn compact_step_watermark_advances ( ) {
53+ async fn compact_step_reclaims_after_deletes ( ) {
5454 let db = fresh_db ( ) . await ;
5555
56- // Write 100 keys and delete half to create free pages.
56+ // Write 100 keys and delete half to free pages.
5757 {
5858 let mut txn = db. begin_write ( ) . await . unwrap ( ) ;
5959 for i in 0u32 ..100 {
6060 let key = format ! ( "wm-{i:04}" ) ;
61- txn. put ( key. as_bytes ( ) , b"watermark -test-value" )
61+ txn. put ( key. as_bytes ( ) , b"reclaim -test-value" )
6262 . await
6363 . unwrap ( ) ;
6464 }
@@ -73,35 +73,29 @@ async fn compact_step_watermark_advances() {
7373 txn. commit ( ) . await . unwrap ( ) ;
7474 }
7575
76- // Run compaction in tiny batches of 5 and verify watermark is monotonically
77- // non-decreasing until completion.
78- let budget = CompactBudget :: new ( 5 , 10_000 ) ;
79-
80- let mut prev_watermark: Option < u64 > = None ;
81- let mut iterations = 0usize ;
82- loop {
83- let prog = db. compact_step ( budget) . await . unwrap ( ) ;
84- iterations += 1 ;
85-
86- if let Some ( wm) = prog. watermark {
87- if let Some ( prev) = prev_watermark {
88- assert ! (
89- wm >= prev,
90- "watermark must not decrease: prev={prev} current={wm}"
91- ) ;
92- }
93- prev_watermark = Some ( wm) ;
94- } else {
95- // Watermark cleared = session complete.
96- assert ! ( !prog. more_work) ;
97- break ;
98- }
76+ // Compaction runs to completion in one call (a full rewrite cannot be safely
77+ // chunked across lock releases) and reclaims the freed pages.
78+ let prog = db. compact_step ( CompactBudget :: default ( ) ) . await . unwrap ( ) ;
79+ assert ! ( !prog. more_work, "compaction completes in a single call" ) ;
80+ assert ! (
81+ prog. pages_relocated > 0 ,
82+ "expected reclaimed pages after deleting half the keys"
83+ ) ;
9984
100- if !prog. more_work {
101- break ;
102- }
85+ // A second call has nothing to reclaim.
86+ let prog2 = db. compact_step ( CompactBudget :: default ( ) ) . await . unwrap ( ) ;
87+ assert ! ( !prog2. more_work) ;
88+ assert_eq ! ( prog2. pages_relocated, 0 , "already compact: nothing to do" ) ;
10389
104- assert ! ( iterations < 10_000 , "compaction did not converge" ) ;
90+ // Surviving keys remain readable.
91+ let r = db. begin_read ( ) . await . unwrap ( ) ;
92+ for i in ( 1u32 ..100 ) . step_by ( 2 ) {
93+ let key = format ! ( "wm-{i:04}" ) ;
94+ assert_eq ! (
95+ r. get( key. as_bytes( ) ) . await . unwrap( ) . as_deref( ) ,
96+ Some ( b"reclaim-test-value" . as_slice( ) ) ,
97+ "{key} must survive compaction"
98+ ) ;
10599 }
106100}
107101
@@ -281,52 +275,35 @@ async fn compact_step_reopen_history_consistent() {
281275 ) ;
282276}
283277
284- // ─── Test: an intermediate compact_step preserves the durable free-list ──────
278+ /// `compact_step` runs the same atomic repack as `compact_now`, so it must
279+ /// preserve overflow (large) values rather than failing or corrupting the store.
285280#[ tokio:: test( flavor = "current_thread" ) ]
286- async fn compact_step_preserves_free_list ( ) {
287- // No commit history, so freed pages are immediately reclaimable and tracked
288- // in the durable free-list with no reader/history pinning them.
289- let opts = pagedb:: options:: OpenOptions :: default ( )
290- . with_commit_history_retain ( pagedb:: options:: RetainPolicy :: Disabled ) ;
291- let db = Db :: open_internal_with_options ( MemVfs :: new ( ) , KEK , PAGE , REALM , opts)
281+ async fn compact_step_preserves_large_overflow_values ( ) {
282+ let vfs = MemVfs :: new ( ) ;
283+ let db = Db :: open_internal ( vfs. clone ( ) , KEK , PAGE , REALM )
292284 . await
293285 . unwrap ( ) ;
294286
295- // Build a working set, then delete most of it to populate the free-list.
287+ let big = vec ! [ 0x3Cu8 ; 4096 ] ; // > PAGE/4 → overflow chain
288+ let n = 60u32 ;
296289 {
297290 let mut w = db. begin_write ( ) . await . unwrap ( ) ;
298- for i in 0u32 ..300 {
299- w. put ( format ! ( "k{i:05}" ) . as_bytes ( ) , & [ 1u8 ; 128 ] )
300- . await
301- . unwrap ( ) ;
291+ for i in 0 ..n {
292+ w. put ( format ! ( "k-{i:05}" ) . as_bytes ( ) , & big) . await . unwrap ( ) ;
302293 }
303294 w. commit ( ) . await . unwrap ( ) ;
304295 }
305- {
306- let mut w = db. begin_write ( ) . await . unwrap ( ) ;
307- for i in 0u32 ..250 {
308- w. delete ( format ! ( "k{i:05}" ) . as_bytes ( ) ) . await . unwrap ( ) ;
309- }
310- w. commit ( ) . await . unwrap ( ) ;
311- }
312- let pending_before = db. stats ( ) . await . unwrap ( ) . free_list_pending_entries ;
313- assert ! (
314- pending_before > 0 ,
315- "setup should have populated the free-list; got {pending_before}"
316- ) ;
317296
318- // One intermediate step (small budget so it is NOT the final batch).
319- let prog = db
320- . compact_step ( CompactBudget :: new ( 5 , 10_000 ) )
321- . await
322- . unwrap ( ) ;
323- assert ! ( prog. more_work, "small budget should leave more work" ) ;
297+ let prog = db. compact_step ( CompactBudget :: default ( ) ) . await . unwrap ( ) ;
298+ assert ! ( !prog. more_work, "compaction completes in a single call" ) ;
324299
325- // The pre-existing free-list must survive the intermediate step, not be
326- // wiped — those pages are still reusable by ordinary writes.
327- let pending_after = db. stats ( ) . await . unwrap ( ) . free_list_pending_entries ;
328- assert ! (
329- pending_after >= pending_before,
330- "intermediate compact_step wiped the durable free-list: {pending_before} -> {pending_after}"
331- ) ;
300+ let r = db. begin_read ( ) . await . unwrap ( ) ;
301+ for i in 0 ..n {
302+ let key = format ! ( "k-{i:05}" ) ;
303+ assert_eq ! (
304+ r. get( key. as_bytes( ) ) . await . unwrap( ) . as_deref( ) ,
305+ Some ( big. as_slice( ) ) ,
306+ "large value {key} lost after compact_step repack"
307+ ) ;
308+ }
332309}
0 commit comments