Skip to content

Commit fc167a2

Browse files
fix(rekey): preserve retained historical roots
1 parent 4a6c853 commit fc167a2

3 files changed

Lines changed: 226 additions & 42 deletions

File tree

src/btree/tree/maintenance.rs

Lines changed: 90 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
//! Maintenance walks: rekey under a new epoch and reachable-page collection.
22
3+
use std::collections::BTreeMap;
4+
35
use crate::Result;
46
use crate::errors::PagedbError;
57
use crate::pager::format::page_kind::PageKind;
@@ -21,20 +23,46 @@ impl<V: Vfs> BTree<V> {
2123
///
2224
/// Returns the count of pages touched.
2325
pub async fn rekey_walk(&self) -> Result<u64> {
26+
self.rekey_walk_unique(&mut BTreeMap::new()).await
27+
}
28+
29+
/// Rekey this tree while sharing traversal state with other live roots.
30+
///
31+
/// Retained snapshots commonly share most of their pages. A caller walking
32+
/// several roots supplies one page-kind map so each physical page is
33+
/// authenticated and rewritten exactly once without hiding a cross-kind
34+
/// alias.
35+
pub(crate) async fn rekey_walk_unique(
36+
&self,
37+
visited: &mut BTreeMap<u64, PageKind>,
38+
) -> Result<u64> {
2439
if self.root_page_id == 0 {
2540
return Ok(0);
2641
}
27-
let mut stack: Vec<u64> = vec![self.root_page_id];
42+
let mut stack: Vec<(u64, u64)> = vec![(0, self.root_page_id)];
2843
let mut count: u64 = 0;
29-
while let Some(page_id) = stack.pop() {
44+
while let Some((parent_page_id, page_id)) = stack.pop() {
45+
if is_reserved(page_id) {
46+
return Err(PagedbError::reserved_page_referenced(
47+
parent_page_id,
48+
page_id,
49+
));
50+
}
51+
if let Some(kind) = visited.get(&page_id) {
52+
match kind {
53+
PageKind::BTreeLeaf | PageKind::BTreeInternal => continue,
54+
_ => return Err(PagedbError::IllegalPageKind),
55+
}
56+
}
3057
// Determine kind by reading the node under its own header kind byte.
31-
let (is_leaf, body_bytes) = {
32-
let (g, _page_kind) = self.pager.read_main_node(page_id, self.realm_id).await?;
58+
let (is_leaf, page_kind, body_bytes) = {
59+
let (g, page_kind) = self.pager.read_main_node(page_id, self.realm_id).await?;
3360
let body = g.body();
3461
let header = read_header(&body)?;
3562
let is_leaf = header.kind == NodeKind::Leaf;
36-
(is_leaf, body.to_vec())
63+
(is_leaf, page_kind, body.to_vec())
3764
};
65+
visited.insert(page_id, page_kind);
3866

3967
if is_leaf {
4068
// Collect overflow chains referenced by this leaf.
@@ -45,37 +73,9 @@ impl<V: Vfs> BTree<V> {
4573
..
4674
} = v
4775
{
48-
// Rewrite the root page (`PageKind::OverflowRoot`).
49-
let root_info =
50-
overflow::read_root_page(&self.pager, self.realm_id, *ov_root).await?;
51-
self.pager
52-
.rewrite_page_under_current_epoch(
53-
*ov_root,
54-
self.realm_id,
55-
PageKind::OverflowRoot,
56-
)
76+
count += self
77+
.rekey_overflow_unique(page_id, *ov_root, visited)
5778
.await?;
58-
count += 1;
59-
// Walk and rewrite chain pages (always PageKind::Overflow).
60-
let mut next = root_info.next;
61-
while next != 0 {
62-
let ov_guard = self
63-
.pager
64-
.read_main_page(next, self.realm_id, PageKind::Overflow)
65-
.await?;
66-
let ov_body = ov_guard.body();
67-
let (ov_next, _) = overflow::decode_overflow(&ov_body)?;
68-
drop(ov_guard);
69-
self.pager
70-
.rewrite_page_under_current_epoch(
71-
next,
72-
self.realm_id,
73-
PageKind::Overflow,
74-
)
75-
.await?;
76-
count += 1;
77-
next = ov_next;
78-
}
7979
}
8080
}
8181
// Rewrite the leaf page.
@@ -86,9 +86,13 @@ impl<V: Vfs> BTree<V> {
8686
} else {
8787
// Internal node: push children onto stack.
8888
let internal = internal::Internal::decode(&body_bytes)?;
89-
stack.push(internal.leftmost_child);
89+
if internal.leftmost_child != 0 {
90+
stack.push((page_id, internal.leftmost_child));
91+
}
9092
for entry in &internal.entries {
91-
stack.push(entry.right_child);
93+
if entry.right_child != 0 {
94+
stack.push((page_id, entry.right_child));
95+
}
9296
}
9397
// Rewrite the internal page.
9498
self.pager
@@ -104,6 +108,55 @@ impl<V: Vfs> BTree<V> {
104108
Ok(count)
105109
}
106110

111+
async fn rekey_overflow_unique(
112+
&self,
113+
leaf_page_id: u64,
114+
root: u64,
115+
visited: &mut BTreeMap<u64, PageKind>,
116+
) -> Result<u64> {
117+
if is_reserved(root) {
118+
return Err(PagedbError::reserved_page_referenced(leaf_page_id, root));
119+
}
120+
if let Some(kind) = visited.get(&root) {
121+
return match kind {
122+
PageKind::OverflowRoot => Ok(0),
123+
_ => Err(PagedbError::IllegalPageKind),
124+
};
125+
}
126+
visited.insert(root, PageKind::OverflowRoot);
127+
128+
let root_info = overflow::read_root_page(&self.pager, self.realm_id, root).await?;
129+
self.pager
130+
.rewrite_page_under_current_epoch(root, self.realm_id, PageKind::OverflowRoot)
131+
.await?;
132+
let mut count = 1;
133+
let mut next = root_info.next;
134+
while next != 0 {
135+
if is_reserved(next) {
136+
return Err(PagedbError::reserved_page_referenced(root, next));
137+
}
138+
if let Some(kind) = visited.get(&next) {
139+
return match kind {
140+
PageKind::Overflow => Err(PagedbError::overflow_chain_cycle(root, next)),
141+
_ => Err(PagedbError::IllegalPageKind),
142+
};
143+
}
144+
visited.insert(next, PageKind::Overflow);
145+
let guard = self
146+
.pager
147+
.read_main_page(next, self.realm_id, PageKind::Overflow)
148+
.await?;
149+
let (following, _) = overflow::decode_overflow(guard.body_ref())?;
150+
drop(guard);
151+
self.pager
152+
.rewrite_page_under_current_epoch(next, self.realm_id, PageKind::Overflow)
153+
.await?;
154+
count += 1;
155+
next = following;
156+
}
157+
Ok(count)
158+
}
159+
107160
/// Collect all page IDs reachable from this tree's root (internal nodes,
108161
/// leaves, overflow chains) into `out`. Used by the deep-walk integrity
109162
/// checker to identify orphan pages.

src/txn/db/rekey/main.rs

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
//! Main-database rekey transition and durable intent publication.
22
3+
use std::collections::BTreeMap;
34
use std::sync::atomic::Ordering;
45

56
use subtle::ConstantTimeEq;
@@ -16,7 +17,9 @@ use crate::vfs::Vfs;
1617

1718
#[cfg(test)]
1819
use super::super::core::RekeyTestFault;
19-
use super::super::core::{Db, WriterState, encode_free_list_root, encode_root_ref};
20+
use super::super::core::{
21+
Db, WriterState, decode_commit_meta, encode_free_list_root, encode_root_ref,
22+
};
2023
use super::intent::{intent_proof, migrate_legacy, validate_intent_for_current_cipher};
2124

2225
impl<V: Vfs + Clone> Db<V> {
@@ -192,23 +195,54 @@ impl<V: Vfs + Clone> Db<V> {
192195
// target header was published.
193196
self.pager
194197
.set_active_mk_epoch(target_master_key.clone(), intent.target_mk_epoch);
198+
let mut rewritten = BTreeMap::new();
195199
let main_tree = BTree::open(
196200
self.pager.clone(),
197201
self.realm_id,
198202
state.root_page_id,
199203
state.next_page_id,
200204
self.page_size,
201205
);
202-
main_tree.rekey_walk().await?;
203-
if state.commit_history_root_page_id != 0 {
206+
main_tree.rekey_walk_unique(&mut rewritten).await?;
207+
let retained_history = if state.commit_history_root_page_id != 0 {
204208
let history_tree = BTree::open(
205209
self.pager.clone(),
206210
self.realm_id,
207211
state.commit_history_root_page_id,
208212
state.next_page_id,
209213
self.page_size,
210214
);
211-
history_tree.rekey_walk().await?;
215+
let entries = history_tree.collect_all().await?;
216+
history_tree.rekey_walk_unique(&mut rewritten).await?;
217+
entries
218+
.into_iter()
219+
.map(|(_, value)| decode_commit_meta(&value))
220+
.collect::<Result<Vec<_>>>()?
221+
} else {
222+
Vec::new()
223+
};
224+
for historical in retained_history {
225+
for root_page_id in [
226+
historical.active_root_page_id,
227+
historical.catalog_root_page_id,
228+
] {
229+
if root_page_id == 0 {
230+
continue;
231+
}
232+
BTree::open(
233+
self.pager.clone(),
234+
self.realm_id,
235+
root_page_id,
236+
historical.next_page_id,
237+
self.page_size,
238+
)
239+
.rekey_walk_unique(&mut rewritten)
240+
.await?;
241+
}
242+
// Historical readers traverse only retained data and catalog
243+
// roots. The recorded free-list root is writer-only metadata whose
244+
// superseded chain pages may already have been recycled. Only the
245+
// current header's live chain below is safe to follow.
212246
}
213247
if state.free_list_root_page_id != 0 {
214248
let (_, chain_pages) = crate::pager::freelist::read_chain(

tests/rekey_basic.rs

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
//! Rekey integration tests: main-db and immutable-segment transitions.
22
3+
use pagedb::options::RetainPolicy;
34
use pagedb::vfs::memory::MemVfs;
4-
use pagedb::{Db, Evictable, PagedbError, RealmId, SegmentKind, SegmentPageKind};
5+
use pagedb::{
6+
Db, Evictable, OpenOptions, PagedbError, RealmId, RealmQuotas, SegmentKind, SegmentPageKind,
7+
};
58

69
const PAGE: usize = 4096;
710
const KEK0: [u8; 32] = [0xAA; 32];
@@ -57,6 +60,100 @@ async fn rekey_main_db_only() {
5760
drop(rx);
5861
}
5962

63+
/// Rekey must rewrite roots named only by retained commit-history metadata
64+
/// before the source epoch is retired.
65+
#[tokio::test(flavor = "current_thread")]
66+
async fn rekey_preserves_retained_historical_snapshots_after_reopen() {
67+
let vfs = MemVfs::new();
68+
let options = OpenOptions::default().with_commit_history_retain(RetainPolicy::Unbounded);
69+
let db = Db::open_internal_with_options(vfs.clone(), KEK0, PAGE, REALM, options)
70+
.await
71+
.unwrap();
72+
db.set_realm_quotas(
73+
REALM,
74+
RealmQuotas {
75+
max_pages: Some(10),
76+
..RealmQuotas::default()
77+
},
78+
)
79+
.await
80+
.unwrap();
81+
let historical_large = vec![0xA5; PAGE * 3];
82+
let latest_large = vec![0x5A; PAGE * 2];
83+
84+
let first = {
85+
let mut tx = db.begin_write().await.unwrap();
86+
for index in 0u16..256 {
87+
tx.put(
88+
format!("history-key-{index:04}").as_bytes(),
89+
&index.to_le_bytes(),
90+
)
91+
.await
92+
.unwrap();
93+
}
94+
tx.put(b"versioned", b"before-rekey").await.unwrap();
95+
tx.put(b"historical-overflow", &historical_large)
96+
.await
97+
.unwrap();
98+
tx.commit().await.unwrap()
99+
};
100+
db.set_realm_quotas(
101+
REALM,
102+
RealmQuotas {
103+
max_pages: Some(20),
104+
..RealmQuotas::default()
105+
},
106+
)
107+
.await
108+
.unwrap();
109+
{
110+
let mut tx = db.begin_write().await.unwrap();
111+
tx.put(b"versioned", b"latest").await.unwrap();
112+
tx.put(b"historical-overflow", &latest_large).await.unwrap();
113+
tx.commit().await.unwrap();
114+
}
115+
116+
db.rekey_db(KEK0, 1).await.unwrap();
117+
drop(db);
118+
119+
let reopened = Db::open_existing(vfs, KEK0, PAGE, REALM).await.unwrap();
120+
let historical = reopened.begin_read_at(first).await.unwrap();
121+
assert_eq!(
122+
historical.get(b"versioned").await.unwrap().as_deref(),
123+
Some(b"before-rekey".as_slice())
124+
);
125+
assert_eq!(
126+
historical
127+
.get(b"historical-overflow")
128+
.await
129+
.unwrap()
130+
.as_deref(),
131+
Some(historical_large.as_slice())
132+
);
133+
let first_index = 0u16.to_le_bytes();
134+
assert_eq!(
135+
historical
136+
.get(b"history-key-0000")
137+
.await
138+
.unwrap()
139+
.as_deref(),
140+
Some(first_index.as_slice())
141+
);
142+
assert!(matches!(
143+
historical.open_segment("absent").await,
144+
Err(PagedbError::NotFound)
145+
));
146+
let latest = reopened.begin_read().await.unwrap();
147+
assert_eq!(
148+
latest.get(b"versioned").await.unwrap().as_deref(),
149+
Some(b"latest".as_slice())
150+
);
151+
assert_eq!(
152+
latest.get(b"historical-overflow").await.unwrap().as_deref(),
153+
Some(latest_large.as_slice())
154+
);
155+
}
156+
60157
// ── Test 2 ─────────────────────────────────────────────────────────────────
61158

62159
/// Linked immutable segments are replaced under the target epoch while

0 commit comments

Comments
 (0)