Skip to content

Commit 272ee42

Browse files
committed
feat(pager): add apply-journal sidecar file support
Introduce FileKey::ApplyJournal([u8; 16]) so the Pager can route AEAD-authenticated reads and writes to applyjournal/<hex(id)> files independently of main.db pages and catalog-tracked segments. Add per-journal SegmentNonceGen instances (keyed by journal_id) so each sidecar's nonce space is isolated from every other file under the same key. Expose stage_journal_page, flush_journal, read_journal_page, and remove_journal helpers that parallel the existing segment API.
1 parent dad7f26 commit 272ee42

2 files changed

Lines changed: 88 additions & 1 deletion

File tree

src/pager/cache.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ use std::sync::Arc;
1414
pub enum FileKey {
1515
Main,
1616
Segment([u8; 16]),
17+
/// Apply-journal sidecar at `applyjournal/<hex(id)>`. A receiver-local,
18+
/// AEAD-authenticated file written before an `apply_incremental` header
19+
/// swap; it is neither a `main.db` page nor a catalog-tracked segment.
20+
ApplyJournal([u8; 16]),
1721
}
1822

1923
/// Bytes plus pin count. `bytes` holds the decrypted full-page buffer

src/pager/core.rs

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)