@@ -37,6 +37,8 @@ impl<V: Vfs + Clone> Db<V> {
3737 } ;
3838
3939 let ( target_commit, next_page_id) = { ( txn. commit_id ( ) . 0 , txn. next_page_id ( ) ) } ;
40+ let target_active_root_page_id = txn. root_page_id ( ) ;
41+ let target_catalog_root_page_id = txn. catalog_root_page_id ( ) ;
4042
4143 // Collect live segment ids from the pinned catalog snapshot.
4244 let segments = txn. list_segments ( "" ) . await ?;
@@ -62,6 +64,8 @@ impl<V: Vfs + Clone> Db<V> {
6264 next_page_id_at_target : next_page_id,
6365 segments_count,
6466 realm_id : self . realm_id . 0 ,
67+ target_active_root_page_id,
68+ target_catalog_root_page_id,
6569 } ;
6670
6771 let src_root = get_vfs_root ( & * self . vfs ) ?;
@@ -168,6 +172,8 @@ impl<V: Vfs + Clone> Db<V> {
168172
169173 let target_commit = txn. commit_id ( ) . 0 ;
170174 let target_next_page_id = txn. next_page_id ( ) ;
175+ let target_active_root_page_id = txn. root_page_id ( ) ;
176+ let target_catalog_root_page_id = txn. catalog_root_page_id ( ) ;
171177
172178 // Get base snapshot's next_page_id by reading the commit history.
173179 let base_txn_result = self . begin_read_at ( base_commit) . await ;
@@ -225,6 +231,8 @@ impl<V: Vfs + Clone> Db<V> {
225231 next_page_id_at_target : target_next_page_id,
226232 segments_count,
227233 realm_id : self . realm_id . 0 ,
234+ target_active_root_page_id,
235+ target_catalog_root_page_id,
228236 } ;
229237
230238 let src_root = get_vfs_root ( & * self . vfs ) ?;
@@ -253,11 +261,10 @@ impl<V: Vfs + Clone> Db<V> {
253261 & self ,
254262 src_path : & std:: path:: Path ,
255263 ) -> crate :: Result < crate :: snapshot:: ApplyStats > {
256- use crate :: pager:: format:: data_page:: ENVELOPE_OVERHEAD ;
257- use crate :: pager:: format:: page_kind:: PageKind ;
258264 use crate :: pager:: header:: commit_header;
259265 use crate :: recovery:: journal:: {
260- ApplyJournalRecord , JournalAction , encode_apply_journal, execute_journal_actions,
266+ ApplyJournalRecord , JournalAction , encode_journal_id, encode_journal_pages,
267+ execute_journal_actions,
261268 } ;
262269 use crate :: snapshot:: apply:: { apply_delta_pages, stage_snapshot_segments} ;
263270 use crate :: txn:: mode:: DbMode ;
@@ -315,31 +322,32 @@ impl<V: Vfs + Clone> Db<V> {
315322
316323 let mut state = self . writer . lock ( ) . await ;
317324
318- // Select the journal slot by parity of the current version counter.
319- // Even version → slot page 2; odd version → slot page 3.
320- let journal_version = state . seq ;
321- let journal_page_id : u64 = if journal_version % 2 == 0 { 2 } else { 3 } ;
322-
323- // Write the journal record to the selected slot page via the Pager's
324- // AEAD path. This ensures the journal is authenticated under the same
325- // keys as all other pages.
326- if !actions . is_empty ( ) {
327- let body_len = page_size - ENVELOPE_OVERHEAD ;
325+ // Write the journal record to a fresh apply-journal sidecar via the
326+ // Pager AEAD path. A fresh, never-reused `journal_id` guarantees the
327+ // sidecar's nonce space never collides with another file's under one
328+ // key. The sidecar may span any number of pages, so the promotion set
329+ // is unbounded — no single-page ceiling. The 16-byte id is carried in
330+ // the header's `apply_journal_root` fields after the swap.
331+ let journal_id = if actions . is_empty ( ) {
332+ [ 0u8 ; 16 ]
333+ } else {
334+ let id = self . next_segment_id ( ) ;
328335 let record = ApplyJournalRecord {
329336 target_commit_id : new_commit_id,
330337 actions : actions. clone ( ) ,
331338 } ;
332- let body = encode_apply_journal ( & record, body_len) ?;
333- self . pager
334- . write_main_page (
335- journal_page_id,
336- self . realm_id ,
337- PageKind :: ApplyJournal ,
338- & body,
339- )
340- . await ?;
341- self . pager . flush_main ( self . realm_id ) . await ?;
342- }
339+ let pages = encode_journal_pages ( & record, page_size) ?;
340+ self . vfs . mkdir_all ( "applyjournal" ) . await ?;
341+ for ( page_id, body) in pages. iter ( ) . enumerate ( ) {
342+ self . pager
343+ . stage_journal_page ( id, page_id as u64 , self . realm_id , body)
344+ . await ?;
345+ }
346+ self . pager . flush_journal ( id, self . realm_id ) . await ?;
347+ self . vfs . sync_dir ( "applyjournal" ) . await . ok ( ) ;
348+ id
349+ } ;
350+ let ( journal_root_page_id, journal_root_version) = encode_journal_id ( & journal_id) ;
343351
344352 // Commit the A/B header with the journal root pointing at the slot we
345353 // just wrote. After this commit, a crash-recovery replay can re-execute
@@ -348,16 +356,18 @@ impl<V: Vfs + Clone> Db<V> {
348356 let new_seq = state. seq + 1 ;
349357 let counter_anchor = self . pager . pending_anchor ( ) ;
350358
359+ // Install the target trees the producer shipped in the manifest. The
360+ // delta pages just written to main.db contain these root pages; pointing
361+ // the header at them is what advances the data and catalog trees past the
362+ // base snapshot (without this, incrementally-applied rows and segments
363+ // are unreachable from the follower's catalog).
364+ let new_root_page_id = manifest. target_active_root_page_id ;
365+ let new_catalog_root_page_id = manifest. target_catalog_root_page_id ;
366+
351367 let mut catalog_root_bytes = [ 0u8 ; 16 ] ;
352- catalog_root_bytes[ ..8 ] . copy_from_slice ( & state . catalog_root_page_id . to_le_bytes ( ) ) ;
368+ catalog_root_bytes[ ..8 ] . copy_from_slice ( & new_catalog_root_page_id . to_le_bytes ( ) ) ;
353369 catalog_root_bytes[ 8 ..] . copy_from_slice ( & new_commit_id. to_le_bytes ( ) ) ;
354370
355- let journal_root_page_id_for_header = if actions. is_empty ( ) {
356- 0
357- } else {
358- journal_page_id
359- } ;
360-
361371 let fields_with_journal = MainDbHeaderFields {
362372 format_version : 1 ,
363373 cipher_id : self . cipher_id . as_byte ( ) ,
@@ -367,14 +377,14 @@ impl<V: Vfs + Clone> Db<V> {
367377 kek_salt : self . kek_salt ,
368378 mk_epoch : self . mk_epoch . load ( std:: sync:: atomic:: Ordering :: SeqCst ) ,
369379 seq : new_seq,
370- active_root_page_id : manifest . next_page_id_at_target ,
380+ active_root_page_id : new_root_page_id ,
371381 active_root_txn_id : new_commit_id,
372382 counter_anchor,
373383 commit_id : crate :: CommitId ( new_commit_id) ,
374384 free_list_root : [ 0 ; 16 ] ,
375385 catalog_root : catalog_root_bytes,
376- apply_journal_root_page_id : journal_root_page_id_for_header ,
377- apply_journal_root_version : journal_version ,
386+ apply_journal_root_page_id : journal_root_page_id ,
387+ apply_journal_root_version : journal_root_version ,
378388 commit_history_root_page_id : 0 ,
379389 commit_history_root_version : 0 ,
380390 restore_mode : 0 ,
@@ -417,7 +427,7 @@ impl<V: Vfs + Clone> Db<V> {
417427 kek_salt : self . kek_salt ,
418428 mk_epoch : self . mk_epoch . load ( std:: sync:: atomic:: Ordering :: SeqCst ) ,
419429 seq : new_seq2,
420- active_root_page_id : new_next_page_id ,
430+ active_root_page_id : new_root_page_id ,
421431 active_root_txn_id : new_commit_id,
422432 counter_anchor : counter_anchor2,
423433 commit_id : crate :: CommitId ( new_commit_id) ,
@@ -444,10 +454,17 @@ impl<V: Vfs + Clone> Db<V> {
444454 self . pager . commit_anchor ( counter_anchor2) ?;
445455 state. active_slot = new_slot2;
446456 state. seq = new_seq2;
457+
458+ // The journal root is cleared and durable; the sidecar is no longer
459+ // needed. Remove it (a crash before this point leaves the sidecar,
460+ // which the next open's replay re-runs idempotently then removes).
461+ self . pager . remove_journal ( journal_id) . await ?;
447462 }
448463
449464 state. latest_commit_id = new_commit_id;
450465 state. next_page_id = new_next_page_id;
466+ state. root_page_id = new_root_page_id;
467+ state. catalog_root_page_id = new_catalog_root_page_id;
451468 self . latest_commit
452469 . store ( new_commit_id, std:: sync:: atomic:: Ordering :: SeqCst ) ;
453470 self . publish_snapshot ( & state) ;
0 commit comments