Skip to content

Commit dad7f26

Browse files
authored
Merge pull request #4 from emanzx/fix/compact-now-send
fix(compaction): keep compact_now() Send + fix large-payload corruption (closes #3)
2 parents 42789f7 + 1312214 commit dad7f26

16 files changed

Lines changed: 628 additions & 745 deletions

File tree

src/btree/overflow.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,14 @@ use crate::{RealmId, Result};
2626
/// Header length for chain pages (non-root): `next[8] || data_len[4]`.
2727
pub const OVERFLOW_HEADER_LEN: usize = 12;
2828

29+
/// Values longer than this are stored as an overflow chain rather than inline in
30+
/// a leaf. Shared by the `put` path and `bulk_load` so a repack reproduces the
31+
/// same inline/overflow split the original writes produced.
32+
#[must_use]
33+
pub fn inline_value_threshold(page_size: usize) -> usize {
34+
page_size / 4
35+
}
36+
2937
/// Extra bytes at the start of a v2 root body before the standard header:
3038
/// `refcount[4]`.
3139
const OVERFLOW_ROOT_V2_PREFIX: usize = 4;

src/btree/tree/bulk.rs

Lines changed: 54 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,26 @@
11
//! Bulk-load: build a dense tree from sorted pairs without `CoW` overhead.
22
3+
use std::sync::Arc;
4+
35
use crate::Result;
46
use crate::errors::PagedbError;
57
use crate::vfs::Vfs;
68

79
use crate::btree::leaf::{Leaf, LeafValue};
810
use crate::btree::node;
11+
use crate::btree::overflow;
912

1013
use super::core::BTree;
1114

1215
impl<V: Vfs> BTree<V> {
1316
/// Bulk-load sorted `(key, value)` pairs into a freshly-created tree
1417
/// without any `CoW` overhead. The tree must be empty (`root_page_id == 0`).
1518
///
16-
/// All records are first placed into as few leaves as needed, then internal
17-
/// nodes are built bottom-up. Each page is written exactly once; no freed
18-
/// pages are generated. The resulting layout is dense and compact.
19-
///
20-
/// Overflow values are NOT supported in bulk-load; callers must ensure all
21-
/// values fit inline (`value.len()` ≤ `page_size` / 4). For compaction use-
22-
/// cases, the pairs come from `collect_range` which already resolves
23-
/// overflow chains into inline bytes.
19+
/// Values larger than the inline threshold (`page_size / 4`) are spilled to
20+
/// overflow chains, exactly as the `put` path does, so a dense repack
21+
/// reproduces the original storage shape. Records are then packed into as few
22+
/// leaves as needed and internal nodes are built bottom-up. Each leaf/internal
23+
/// page is written exactly once; the layout is dense and compact.
2424
///
2525
/// Returns `Err` if the tree is not empty.
2626
#[allow(clippy::too_many_lines)]
@@ -34,82 +34,70 @@ impl<V: Vfs> BTree<V> {
3434
return Ok(());
3535
}
3636

37-
// Build leaves greedily: pack as many records as fit into each leaf.
38-
// No sibling pointers yet — we'll wire them up at the end.
39-
let mut leaves: Vec<(u64, Vec<u8>)> = Vec::new(); // (page_id, first_key)
40-
41-
let body_cap = node::body_capacity(self.page_size);
42-
let mut current_leaf = Leaf::new();
37+
// Resolve each value to its stored form, spilling values past the inline
38+
// threshold to overflow chains (same threshold as `put`). Inlining an
39+
// oversized value would exceed leaf capacity and fail the encode.
40+
let realm = self.realm_id;
41+
let ps = self.page_size;
42+
let pager = Arc::clone(&self.pager);
43+
let inline_threshold = overflow::inline_value_threshold(ps);
44+
let mut records: Vec<(Vec<u8>, LeafValue)> = Vec::with_capacity(pairs.len());
45+
for (k, v) in pairs {
46+
let value = if v.len() > inline_threshold {
47+
let total_len = v.len() as u64;
48+
let root_page_id =
49+
overflow::write_chain(&pager, realm, &v, ps, &mut || self.allocate_page())
50+
.await?;
51+
LeafValue::Overflow {
52+
total_len,
53+
root_page_id,
54+
}
55+
} else {
56+
LeafValue::Inline(v)
57+
};
58+
records.push((k, value));
59+
}
4360

44-
let flush_leaf = |leaf: &Leaf, page_id: u64, next_id: u64| {
45-
let _ = (leaf, page_id, next_id); // will write below
46-
};
47-
let _ = flush_leaf; // suppress unused-variable lint (closure is a placeholder)
61+
let body_cap = node::body_capacity(ps);
4862

49-
// First pass: group records into leaves.
50-
let mut leaf_groups: Vec<Vec<(Vec<u8>, Vec<u8>)>> = Vec::new();
51-
for (k, v) in &pairs {
52-
let entry_size = {
53-
let suffix_len = k.len(); // conservative: no prefix compression yet
54-
2 + suffix_len + 2 + v.len() // slot entry (inline value)
55-
};
56-
// Rough check: header + slot_dir entry + record body
63+
// First pass: group records into leaves, sized by encoded record width.
64+
let mut leaf_groups: Vec<Vec<(Vec<u8>, LeafValue)>> = Vec::new();
65+
let mut current: Vec<(Vec<u8>, LeafValue)> = Vec::new();
66+
for (k, value) in records {
67+
// New record's body contribution: suffix-len field + key + value.
68+
// (No prefix compression at build time, so suffix == full key.)
69+
let entry_size = 2 + k.len() + value.encoded_size();
5770
let projected = node::HEADER_LEN
58-
+ (current_leaf.records.len() + 1) * 2
59-
+ current_leaf
60-
.records
71+
+ (current.len() + 1) * 2
72+
+ current
6173
.iter()
6274
.map(|(ck, cv)| 2 + ck.len() + cv.encoded_size())
6375
.sum::<usize>()
6476
+ entry_size;
65-
if projected > body_cap && !current_leaf.records.is_empty() {
66-
leaf_groups.push(
67-
std::mem::take(&mut current_leaf.records)
68-
.into_iter()
69-
.map(|(lk, lv)| {
70-
let vbytes = match lv {
71-
LeafValue::Inline(b) => b,
72-
LeafValue::Overflow { .. } => Vec::new(),
73-
};
74-
(lk, vbytes)
75-
})
76-
.collect(),
77-
);
78-
current_leaf = Leaf::new();
77+
if projected > body_cap && !current.is_empty() {
78+
leaf_groups.push(std::mem::take(&mut current));
7979
}
80-
current_leaf.upsert(k, LeafValue::Inline(v.clone()));
80+
current.push((k, value));
8181
}
82-
if !current_leaf.records.is_empty() {
83-
leaf_groups.push(
84-
std::mem::take(&mut current_leaf.records)
85-
.into_iter()
86-
.map(|(lk, lv)| {
87-
let vbytes = match lv {
88-
LeafValue::Inline(b) => b,
89-
LeafValue::Overflow { .. } => Vec::new(),
90-
};
91-
(lk, vbytes)
92-
})
93-
.collect(),
94-
);
82+
if !current.is_empty() {
83+
leaf_groups.push(current);
9584
}
9685

97-
// Second pass: allocate page_ids and write leaves with correct sibling pointers.
86+
// Second pass: allocate page ids and write leaves with sibling links.
87+
// Input is sorted and grouping preserves order, so each leaf's records
88+
// are already in key order.
89+
let mut leaves: Vec<(u64, Vec<u8>)> = Vec::new(); // (page_id, first_key)
9890
let n_leaves = leaf_groups.len();
9991
let page_ids: Vec<u64> = (0..n_leaves).map(|_| self.allocate_page()).collect();
10092

101-
for (i, group) in leaf_groups.iter().enumerate() {
102-
let mut leaf = Leaf {
93+
for (i, group) in leaf_groups.into_iter().enumerate() {
94+
let first_key = group.first().map(|(k, _)| k.clone()).unwrap_or_default();
95+
let leaf = Leaf {
10396
left_sibling: if i == 0 { 0 } else { page_ids[i - 1] },
10497
right_sibling: if i + 1 < n_leaves { page_ids[i + 1] } else { 0 },
105-
records: group
106-
.iter()
107-
.map(|(k, v)| (k.clone(), LeafValue::Inline(v.clone())))
108-
.collect(),
98+
records: group,
10999
};
110-
leaf.records.sort_by(|(a, _), (b, _)| a.cmp(b)); // already sorted
111100
self.write_leaf(page_ids[i], &leaf).await?;
112-
let first_key = group.first().map(|(k, _)| k.clone()).unwrap_or_default();
113101
leaves.push((page_ids[i], first_key));
114102
}
115103

src/btree/tree/write.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ impl<V: Vfs> BTree<V> {
1717
/// Insert or overwrite a key-value pair.
1818
pub async fn put(&mut self, key: &[u8], value: &[u8]) -> Result<()> {
1919
// Build the leaf value — inline if small enough, overflow chain otherwise.
20-
let leaf_value = if value.len() > self.page_size / 4 {
20+
let leaf_value = if value.len() > overflow::inline_value_threshold(self.page_size) {
2121
let realm = self.realm_id;
2222
let ps = self.page_size;
2323
let pager = Arc::clone(&self.pager);

src/catalog/codec.rs

Lines changed: 4 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -42,46 +42,13 @@ pub enum CatalogRowKind {
4242
/// catalog, reader pins are maintained in-memory only and the writer process
4343
/// must be trusted to honor the catalog pins.
4444
ReaderPin = 0x06,
45-
/// Incremental compaction watermark. Singleton row; key is `[0x07]`.
46-
/// Value: `target_root[8] || frontier_page_id[8] || started_at_commit_id[8] ||
47-
/// total_pages_estimate[8]` = 32 bytes.
48-
///
49-
/// Present only while an incremental compaction is in progress. Cleared
50-
/// atomically with the final compaction commit. On `Db::open`, a present
51-
/// row means a prior compaction was interrupted; the embedder must call
52-
/// `compact_step` again to resume — `Db::open` does NOT auto-resume.
45+
/// Reserved (`0x07`). Older builds wrote an incremental-compaction watermark
46+
/// here; compaction is now a single atomic operation and never writes it.
47+
/// Retained as a row-kind boundary and so any legacy row is recognised and
48+
/// dropped during compaction.
5349
CompactionState = 0x07,
5450
}
5551

56-
/// Incremental compaction watermark persisted in the catalog.
57-
///
58-
/// Present while a `compact_step` session is in progress. The embedder must
59-
/// call `compact_step` again to resume after a crash; `Db::open` does not
60-
/// auto-resume.
61-
///
62-
/// Encoding: `target_root[8] || frontier_page_id[8] || started_at_commit_id[8] ||
63-
/// total_pages_estimate[8] || frontier_key_len[4] || frontier_key[frontier_key_len]`
64-
/// (minimum 32 bytes; variable total).
65-
#[derive(Debug, Clone, PartialEq, Eq)]
66-
pub struct CompactionStateRow {
67-
/// Root page id of the main B+ tree at the time compaction started.
68-
/// Used as the source snapshot to read from during each step.
69-
pub target_root: u64,
70-
/// The `next_page_id` of the partially-built compacted tree after the last
71-
/// committed batch. Used to estimate progress and as the initial `next_page_id`
72-
/// when no free-list pages are available.
73-
pub frontier_page_id: u64,
74-
/// The `commit_id` at which the compaction session began.
75-
pub started_at_commit_id: u64,
76-
/// Estimated total pages in the source tree (for progress reporting).
77-
pub total_pages_estimate: u64,
78-
/// The key (exclusive lower bound) from which the next step should begin
79-
/// reading pairs. Empty means start from the beginning.
80-
pub frontier_key: Vec<u8>,
81-
}
82-
83-
pub const COMPACTION_STATE_FIXED_LEN: usize = 36; // 4 × u64 + 1 × u32 length prefix
84-
8552
/// Rekey watermark persisted in the catalog during an online rekey operation.
8653
/// A present row means a rekey is in flight or was interrupted by a crash.
8754
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -246,61 +213,6 @@ impl Catalog {
246213
[CatalogRowKind::CompactionState as u8]
247214
}
248215

249-
/// Compaction-state row key: `[0x07]` (singleton).
250-
#[must_use]
251-
pub fn compaction_state_key() -> [u8; 1] {
252-
[CatalogRowKind::CompactionState as u8]
253-
}
254-
255-
/// Encode a `CompactionStateRow`. Returns a variable-length `Vec<u8>`.
256-
pub fn encode_compaction_state(r: &CompactionStateRow) -> Result<Vec<u8>> {
257-
let key_len = r.frontier_key.len();
258-
let klen32 = u32::try_from(key_len).map_err(|_| PagedbError::ManifestTooLarge)?;
259-
let mut o = Vec::with_capacity(COMPACTION_STATE_FIXED_LEN + key_len);
260-
o.extend_from_slice(&r.target_root.to_le_bytes());
261-
o.extend_from_slice(&r.frontier_page_id.to_le_bytes());
262-
o.extend_from_slice(&r.started_at_commit_id.to_le_bytes());
263-
o.extend_from_slice(&r.total_pages_estimate.to_le_bytes());
264-
o.extend_from_slice(&klen32.to_le_bytes());
265-
o.extend_from_slice(&r.frontier_key);
266-
Ok(o)
267-
}
268-
269-
/// Decode a `CompactionStateRow` from bytes.
270-
pub fn decode_compaction_state(bytes: &[u8]) -> Result<CompactionStateRow> {
271-
if bytes.len() < COMPACTION_STATE_FIXED_LEN {
272-
return Err(PagedbError::corruption(
273-
crate::errors::CorruptionDetail::HeaderUnverifiable,
274-
));
275-
}
276-
let read_u64 = |off: usize| {
277-
let mut b = [0u8; 8];
278-
b.copy_from_slice(&bytes[off..off + 8]);
279-
u64::from_le_bytes(b)
280-
};
281-
let target_root = read_u64(0);
282-
let frontier_page_id = read_u64(8);
283-
let started_at_commit_id = read_u64(16);
284-
let total_pages_estimate = read_u64(24);
285-
let mut klen_buf = [0u8; 4];
286-
klen_buf.copy_from_slice(&bytes[32..36]);
287-
let key_len = u32::from_le_bytes(klen_buf) as usize;
288-
if bytes.len() < COMPACTION_STATE_FIXED_LEN + key_len {
289-
return Err(PagedbError::corruption(
290-
crate::errors::CorruptionDetail::HeaderUnverifiable,
291-
));
292-
}
293-
let frontier_key =
294-
bytes[COMPACTION_STATE_FIXED_LEN..COMPACTION_STATE_FIXED_LEN + key_len].to_vec();
295-
Ok(CompactionStateRow {
296-
target_root,
297-
frontier_page_id,
298-
started_at_commit_id,
299-
total_pages_estimate,
300-
frontier_key,
301-
})
302-
}
303-
304216
/// Encode a `ReaderPinValue` as 41 bytes.
305217
#[must_use]
306218
pub fn encode_reader_pin(v: &ReaderPinValue) -> [u8; READER_PIN_VALUE_LEN] {

src/catalog/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@
44
55
pub mod codec;
66

7-
pub use codec::{Catalog, CatalogRowKind, CompactionStateRow, ReaderPinValue, RekeyStateRow};
7+
pub use codec::{Catalog, CatalogRowKind, ReaderPinValue, RekeyStateRow};

0 commit comments

Comments
 (0)