@@ -132,7 +132,7 @@ impl PagerInner {
132132 pub ( crate ) fn cache_for_key ( & self , file : FileKey ) -> & parking_lot:: Mutex < PageCache > {
133133 match file {
134134 FileKey :: Main => & self . buffer_pool ,
135- FileKey :: Segment ( _) => & self . segment_cache ,
135+ FileKey :: Segment ( _) | FileKey :: ApplyJournal ( _ ) => & self . segment_cache ,
136136 }
137137 }
138138
@@ -166,6 +166,10 @@ pub struct Pager<V: Vfs> {
166166 dek_lru : parking_lot:: Mutex < DekLru > ,
167167 main_nonce : parking_lot:: Mutex < MainDbNonceGen > ,
168168 segment_nonces : parking_lot:: Mutex < BTreeMap < [ u8 ; 16 ] , SegmentNonceGen > > ,
169+ /// Per-journal-sidecar nonce generators. Each apply allocates a fresh,
170+ /// never-reused `journal_id`, so a generator seeded from that id never
171+ /// collides with another journal's nonces under one key.
172+ journal_nonces : parking_lot:: Mutex < BTreeMap < [ u8 ; 16 ] , SegmentNonceGen > > ,
169173 pub ( crate ) inner : Arc < PagerInner > ,
170174 /// Retries on AEAD failure before surfacing `ChecksumFailure`. Non-zero
171175 /// only in Observer mode to absorb torn reads from a concurrent writer.
@@ -228,6 +232,7 @@ impl<V: Vfs> Pager<V> {
228232 dek_lru : parking_lot:: Mutex :: new ( DekLru :: with_capacity ( cfg. dek_lru_capacity ) ) ,
229233 main_nonce : parking_lot:: Mutex :: new ( main_nonce) ,
230234 segment_nonces : parking_lot:: Mutex :: new ( BTreeMap :: new ( ) ) ,
235+ journal_nonces : parking_lot:: Mutex :: new ( BTreeMap :: new ( ) ) ,
231236 files : AsyncMutex :: new ( BTreeMap :: new ( ) ) ,
232237 inner,
233238 active_epoch : AtomicU64 :: new ( initial_epoch) ,
@@ -355,6 +360,75 @@ impl<V: Vfs> Pager<V> {
355360 Ok ( page_id)
356361 }
357362
363+ /// Stage an apply-journal sidecar page into the cache as dirty. `page_id`
364+ /// is the 0-based page index within `applyjournal/<hex(journal_id)>`.
365+ #[ allow( clippy:: unused_async) ]
366+ pub async fn stage_journal_page (
367+ & self ,
368+ journal_id : [ u8 ; 16 ] ,
369+ page_id : u64 ,
370+ realm_id : RealmId ,
371+ body_plain : & [ u8 ] ,
372+ ) -> Result < ( ) > {
373+ self . write_page (
374+ FileKey :: ApplyJournal ( journal_id) ,
375+ page_id,
376+ realm_id,
377+ PageKind :: ApplyJournal ,
378+ body_plain,
379+ journal_id,
380+ )
381+ }
382+
383+ /// Flush all dirty pages of an apply-journal sidecar to disk and fsync.
384+ pub async fn flush_journal ( & self , journal_id : [ u8 ; 16 ] , realm_id : RealmId ) -> Result < ( ) > {
385+ self . flush_file (
386+ FileKey :: ApplyJournal ( journal_id) ,
387+ realm_id,
388+ journal_id,
389+ None ,
390+ )
391+ . await
392+ }
393+
394+ /// Read an apply-journal sidecar page, AEAD-verified under `realm_id`.
395+ pub async fn read_journal_page (
396+ & self ,
397+ journal_id : [ u8 ; 16 ] ,
398+ page_id : u64 ,
399+ realm_id : RealmId ,
400+ ) -> Result < PageGuard > {
401+ self . read_page (
402+ FileKey :: ApplyJournal ( journal_id) ,
403+ page_id,
404+ realm_id,
405+ PageKind :: ApplyJournal ,
406+ journal_id,
407+ )
408+ . await
409+ }
410+
411+ /// Remove an apply-journal sidecar file and drop all its in-memory state
412+ /// (cache pages, file handle, nonce generator). Called after the journal
413+ /// has been fully replayed and the header pointer cleared.
414+ pub async fn remove_journal ( & self , journal_id : [ u8 ; 16 ] ) -> Result < ( ) > {
415+ let key = FileKey :: ApplyJournal ( journal_id) ;
416+ self . files . lock ( ) . await . remove ( & key) ;
417+ self . inner . cache_for_key ( key) . lock ( ) . clear_file ( key) ;
418+ self . journal_nonces . lock ( ) . remove ( & journal_id) ;
419+ let path = format ! ( "applyjournal/{}" , crate :: hex:: to_hex_lower( & journal_id) ) ;
420+ self . vfs . remove ( & path) . await . ok ( ) ;
421+ Ok ( ( ) )
422+ }
423+
424+ /// Drop an apply-journal sidecar's cached pages without removing the file,
425+ /// forcing subsequent reads to AEAD-decrypt from disk.
426+ #[ cfg( test) ]
427+ pub ( crate ) fn drop_journal_cache ( & self , journal_id : [ u8 ; 16 ] ) {
428+ let key = FileKey :: ApplyJournal ( journal_id) ;
429+ self . inner . cache_for_key ( key) . lock ( ) . clear_file ( key) ;
430+ }
431+
358432 /// Flush all dirty main.db pages to the VFS in physical-id order.
359433 pub async fn flush_main ( & self , realm_id : RealmId ) -> Result < ( ) > {
360434 tracing:: debug!( name = "pager.flush" , "flushing dirty main db pages" ) ;
@@ -734,6 +808,9 @@ impl<V: Vfs> Pager<V> {
734808 let path = match file {
735809 FileKey :: Main => self . cfg . main_db_path . clone ( ) ,
736810 FileKey :: Segment ( id) => format ! ( "seg/{}" , crate :: hex:: to_hex_lower( & id) ) ,
811+ FileKey :: ApplyJournal ( id) => {
812+ format ! ( "applyjournal/{}" , crate :: hex:: to_hex_lower( & id) )
813+ }
737814 } ;
738815 let f = self . vfs . open ( & path, OpenMode :: CreateOrOpen ) . await ?;
739816 let arc = Arc :: new ( AsyncMutex :: new ( f) ) ;
@@ -752,6 +829,11 @@ impl<V: Vfs> Pager<V> {
752829 let nonce_gen = gens. entry ( id) . or_insert_with ( || SegmentNonceGen :: new ( & id) ) ;
753830 nonce_gen. next_nonce ( )
754831 }
832+ FileKey :: ApplyJournal ( id) => {
833+ let mut gens = self . journal_nonces . lock ( ) ;
834+ let nonce_gen = gens. entry ( id) . or_insert_with ( || SegmentNonceGen :: new ( & id) ) ;
835+ nonce_gen. next_nonce ( )
836+ }
755837 }
756838 }
757839}
@@ -765,6 +847,7 @@ fn derive_kind_for_flush(file: FileKey) -> PageKind {
765847 match file {
766848 FileKey :: Main => PageKind :: BTreeLeaf ,
767849 FileKey :: Segment ( _) => PageKind :: SegmentData ,
850+ FileKey :: ApplyJournal ( _) => PageKind :: ApplyJournal ,
768851 }
769852}
770853
0 commit comments