Skip to content

Commit b8f85a7

Browse files
authored
Merge branch 'main' into fix/resume-db-without-checkpoint-url
2 parents 8d0375d + fc1f83a commit b8f85a7

5 files changed

Lines changed: 139 additions & 52 deletions

File tree

crates/storage/src/api/traits.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,15 @@ pub trait StorageWriteBatch: Send {
4343
/// Delete multiple keys from a table.
4444
fn delete_batch(&mut self, table: Table, keys: Vec<Vec<u8>>) -> Result<(), Error>;
4545

46+
/// Delete every key in the half-open range `[from, to)` from a table.
47+
///
48+
/// Unlike [`delete_batch`](Self::delete_batch), the caller does not need to
49+
/// know the keys, so the write cost need not scale with the number of
50+
/// entries covered: RocksDB records a single range tombstone instead of one
51+
/// delete per key. Operations within a batch apply in call order, so a later
52+
/// `put_batch` for a key inside the range still wins.
53+
fn delete_range(&mut self, table: Table, from: &[u8], to: &[u8]) -> Result<(), Error>;
54+
4655
/// Commit the batch, consuming it.
4756
fn commit(self: Box<Self>) -> Result<(), Error>;
4857
}

crates/storage/src/backend/in_memory.rs

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,16 @@ use crate::api::{
88
type TableData = HashMap<Vec<u8>, Vec<u8>>;
99
type StorageData = HashMap<Table, TableData>;
1010

11-
/// Pending operation for a key - last operation wins.
11+
/// Pending operation in a batch, replayed in call order on commit so the last
12+
/// operation touching a key wins (matching how RocksDB applies a `WriteBatch`).
1213
enum PendingOp {
13-
Put(Vec<u8>),
14-
Delete,
14+
Put(Vec<u8>, Vec<u8>),
15+
Delete(Vec<u8>),
16+
/// Delete every key in the half-open range `[from, to)`.
17+
DeleteRange(Vec<u8>, Vec<u8>),
1518
}
1619

17-
type PendingOps = HashMap<Table, HashMap<Vec<u8>, PendingOp>>;
20+
type PendingOps = Vec<(Table, PendingOp)>;
1821

1922
/// In-memory storage backend using HashMaps.
2023
///
@@ -52,7 +55,7 @@ impl StorageBackend for InMemoryBackend {
5255
fn begin_write(&self) -> Result<Box<dyn StorageWriteBatch + 'static>, Error> {
5356
Ok(Box::new(InMemoryWriteBatch {
5457
data: Arc::clone(&self.data),
55-
ops: HashMap::new(),
58+
ops: PendingOps::new(),
5659
}))
5760
}
5861
}
@@ -105,34 +108,41 @@ struct InMemoryWriteBatch {
105108

106109
impl StorageWriteBatch for InMemoryWriteBatch {
107110
fn put_batch(&mut self, table: Table, batch: Vec<(Vec<u8>, Vec<u8>)>) -> Result<(), Error> {
108-
let table_ops = self.ops.entry(table).or_default();
109111
for (key, value) in batch {
110-
table_ops.insert(key, PendingOp::Put(value));
112+
self.ops.push((table, PendingOp::Put(key, value)));
111113
}
112114
Ok(())
113115
}
114116

115117
fn delete_batch(&mut self, table: Table, keys: Vec<Vec<u8>>) -> Result<(), Error> {
116-
let table_ops = self.ops.entry(table).or_default();
117118
for key in keys {
118-
table_ops.insert(key, PendingOp::Delete);
119+
self.ops.push((table, PendingOp::Delete(key)));
119120
}
120121
Ok(())
121122
}
122123

124+
fn delete_range(&mut self, table: Table, from: &[u8], to: &[u8]) -> Result<(), Error> {
125+
let range = PendingOp::DeleteRange(from.to_vec(), to.to_vec());
126+
self.ops.push((table, range));
127+
Ok(())
128+
}
129+
123130
fn commit(self: Box<Self>) -> Result<(), Error> {
124131
let mut guard = self.data.write().map_err(|e| e.to_string())?;
125132

126-
for (table, ops) in self.ops {
133+
for (table, op) in self.ops {
127134
let table_data = guard.get_mut(&table).expect("table exists");
128-
for (key, op) in ops {
129-
match op {
130-
PendingOp::Put(value) => {
131-
table_data.insert(key, value);
132-
}
133-
PendingOp::Delete => {
134-
table_data.remove(&key);
135-
}
135+
match op {
136+
PendingOp::Put(key, value) => {
137+
table_data.insert(key, value);
138+
}
139+
PendingOp::Delete(key) => {
140+
table_data.remove(&key);
141+
}
142+
// Keys are unordered here, so the range is applied by scanning
143+
// the table rather than by seeking to `from`.
144+
PendingOp::DeleteRange(from, to) => {
145+
table_data.retain(|key, _| key < &from || key >= &to);
136146
}
137147
}
138148
}

crates/storage/src/backend/rocksdb.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,16 @@ impl StorageWriteBatch for RocksDBWriteBatch {
167167
Ok(())
168168
}
169169

170+
fn delete_range(&mut self, table: Table, from: &[u8], to: &[u8]) -> Result<(), Error> {
171+
let cf = self
172+
.db
173+
.cf_handle(cf_name(table))
174+
.ok_or_else(|| format!("Column family {} not found", cf_name(table)))?;
175+
176+
self.batch.delete_range_cf(&cf, from, to);
177+
Ok(())
178+
}
179+
170180
fn commit(self: Box<Self>) -> Result<(), Error> {
171181
let mut write_opts = WriteOptions::default();
172182
write_opts.set_sync(false);

crates/storage/src/backend/tests.rs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ pub fn run_backend_tests(backend: &dyn StorageBackend) {
1919
test_nonexistent_key(backend);
2020
test_delete_then_put(backend);
2121
test_put_then_delete(backend);
22+
test_delete_range(backend);
23+
test_delete_range_then_put(backend);
2224
test_multiple_tables(backend);
2325
}
2426

@@ -172,6 +174,66 @@ fn test_put_then_delete(backend: &dyn StorageBackend) {
172174
);
173175
}
174176

177+
fn test_delete_range(backend: &dyn StorageBackend) {
178+
// Keys sort lexicographically, so `b` and `c` fall inside [b, d) while the
179+
// bounds' neighbours `a` and `d` stay: the range is half-open.
180+
{
181+
let mut batch = backend.begin_write().unwrap();
182+
let entries = ["a", "b", "c", "d"]
183+
.iter()
184+
.map(|suffix| (format!("test_range:{suffix}").into_bytes(), b"v".to_vec()))
185+
.collect();
186+
batch.put_batch(Table::LiveChain, entries).unwrap();
187+
batch.commit().unwrap();
188+
}
189+
190+
{
191+
let mut batch = backend.begin_write().unwrap();
192+
batch
193+
.delete_range(Table::LiveChain, b"test_range:b", b"test_range:d")
194+
.unwrap();
195+
batch.commit().unwrap();
196+
}
197+
198+
let view = backend.begin_read().unwrap();
199+
let mut keys: Vec<_> = view
200+
.prefix_iterator(Table::LiveChain, b"test_range:")
201+
.unwrap()
202+
.map(|entry| entry.unwrap().0)
203+
.collect();
204+
205+
keys.sort();
206+
assert_eq!(keys.len(), 2);
207+
assert_eq!(&*keys[0], b"test_range:a");
208+
assert_eq!(&*keys[1], b"test_range:d");
209+
}
210+
211+
fn test_delete_range_then_put(backend: &dyn StorageBackend) {
212+
// Range delete then put of a key inside the range - put should win.
213+
{
214+
let mut batch = backend.begin_write().unwrap();
215+
let entries = vec![(b"test_range_put:b".to_vec(), b"old".to_vec())];
216+
batch.put_batch(Table::LiveChain, entries).unwrap();
217+
batch.commit().unwrap();
218+
}
219+
220+
{
221+
let mut batch = backend.begin_write().unwrap();
222+
batch
223+
.delete_range(Table::LiveChain, b"test_range_put:a", b"test_range_put:z")
224+
.unwrap();
225+
let entries = vec![(b"test_range_put:b".to_vec(), b"new".to_vec())];
226+
batch.put_batch(Table::LiveChain, entries).unwrap();
227+
batch.commit().unwrap();
228+
}
229+
230+
let view = backend.begin_read().unwrap();
231+
assert_eq!(
232+
view.get(Table::LiveChain, b"test_range_put:b").unwrap(),
233+
Some(b"new".to_vec())
234+
);
235+
}
236+
175237
fn test_multiple_tables(backend: &dyn StorageBackend) {
176238
// Write to different tables
177239
{

crates/storage/src/store.rs

Lines changed: 30 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -909,11 +909,11 @@ impl Store {
909909
.map_or(finalized_slot, |header| {
910910
header.expect("Failed to get block header").slot
911911
});
912-
let pruned_signatures = self
912+
let pruned_below_slot = self
913913
.prune_old_block_signatures(finalized_slot, tip_slot)
914914
.expect("prune old block signatures");
915-
if pruned_signatures > 0 {
916-
info!(pruned_signatures, "Pruned old finalized block signatures");
915+
if pruned_below_slot > 0 {
916+
info!(pruned_below_slot, "Pruned old finalized block signatures");
917917
}
918918
Ok(())
919919
}
@@ -1078,41 +1078,37 @@ impl Store {
10781078
/// reverted, so their signatures are not needed for fork choice, re-org
10791079
/// safety, or re-aggregation once outside the window.
10801080
///
1081-
/// Returns the number of signatures pruned.
1081+
/// Returns the exclusive slot below which signatures were dropped, or 0 when
1082+
/// nothing was pruned. This is a range delete, so the count of removed keys
1083+
/// is not known without reading the table back.
10821084
pub fn prune_old_block_signatures(
10831085
&mut self,
10841086
finalized_slot: u64,
10851087
tip_slot: u64,
1086-
) -> Result<usize, Error> {
1088+
) -> Result<u64, Error> {
10871089
let cutoff = tip_slot.saturating_sub(SIGNATURE_PRUNING_RANGE);
10881090
// Only prune when the whole window is finalized; never touch
1089-
// non-finalized signatures.
1090-
if cutoff > finalized_slot {
1091+
// non-finalized signatures. A zero cutoff covers nothing.
1092+
if cutoff > finalized_slot || cutoff == 0 {
10911093
return Ok(0);
10921094
}
10931095

1094-
let view = self.backend.begin_read().expect("read view");
1095-
1096-
// Keys are slot||root in big-endian slot order, so iteration ascends by
1097-
// slot: take entries below the cutoff and stop at the first one past it.
1098-
let keys_to_delete: Vec<Vec<u8>> = view
1099-
.prefix_iterator(Table::BlockSignatures, &[])
1100-
.expect("iterator")
1101-
.filter_map(|res| res.ok())
1102-
.map(|(key, _)| key.to_vec())
1103-
.take_while(|key| decode_slot_root_key(key).0 < cutoff)
1104-
.collect();
1105-
drop(view);
1096+
// Keys are slot||root in big-endian slot order, so the cutoff's bare
1097+
// slot prefix is an exact upper bound: keys below the cutoff sort
1098+
// before it, and keys at the cutoff sort after it (they extend it with
1099+
// a root). A single range delete drops them all without reading the
1100+
// table (and without walking the tombstones left by earlier prunes).
1101+
let mut batch = self.backend.begin_write().expect("write batch");
1102+
batch
1103+
.delete_range(
1104+
Table::BlockSignatures,
1105+
&0u64.to_be_bytes(),
1106+
&cutoff.to_be_bytes(),
1107+
)
1108+
.expect("delete finalized block signatures");
1109+
batch.commit().expect("commit");
11061110

1107-
let count = keys_to_delete.len();
1108-
if count > 0 {
1109-
let mut batch = self.backend.begin_write().expect("write batch");
1110-
batch
1111-
.delete_batch(Table::BlockSignatures, keys_to_delete)
1112-
.expect("delete finalized block signatures");
1113-
batch.commit().expect("commit");
1114-
}
1115-
Ok(count)
1111+
Ok(cutoff)
11161112
}
11171113

11181114
/// Get the block header by root.
@@ -1930,12 +1926,12 @@ mod tests {
19301926
// tip = range + 10, finalized = range + 5, so cutoff = tip - range = 10.
19311927
let tip_slot = SIGNATURE_PRUNING_RANGE + 10;
19321928
let finalized_slot = SIGNATURE_PRUNING_RANGE + 5;
1933-
let pruned = store
1929+
let pruned_below_slot = store
19341930
.prune_old_block_signatures(finalized_slot, tip_slot)
19351931
.expect("prune");
19361932

19371933
// cutoff = 10: slots 0..9 pruned, slots 10..12 kept (within the window).
1938-
assert_eq!(pruned, 10);
1934+
assert_eq!(pruned_below_slot, 10);
19391935
assert_eq!(count_entries(backend.as_ref(), Table::BlockSignatures), 3);
19401936

19411937
// Oldest signatures are gone, but headers, bodies, and roots stay queryable.
@@ -1965,10 +1961,10 @@ mod tests {
19651961
// cutoff = tip - range > finalized → prune nothing.
19661962
let tip_slot = SIGNATURE_PRUNING_RANGE + 100;
19671963
let finalized_slot = 5;
1968-
let pruned = store
1964+
let pruned_below_slot = store
19691965
.prune_old_block_signatures(finalized_slot, tip_slot)
19701966
.expect("prune");
1971-
assert_eq!(pruned, 0);
1967+
assert_eq!(pruned_below_slot, 0);
19721968
assert_eq!(count_entries(backend.as_ref(), Table::BlockSignatures), 10);
19731969
}
19741970

@@ -1983,8 +1979,8 @@ mod tests {
19831979

19841980
// Early chain: tip < SIGNATURE_PRUNING_RANGE → cutoff saturates to 0,
19851981
// so nothing is old enough to prune even though slots are finalized.
1986-
let pruned = store.prune_old_block_signatures(9, 9).expect("prune");
1987-
assert_eq!(pruned, 0);
1982+
let pruned_below_slot = store.prune_old_block_signatures(9, 9).expect("prune");
1983+
assert_eq!(pruned_below_slot, 0);
19881984
assert_eq!(count_entries(backend.as_ref(), Table::BlockSignatures), 10);
19891985
}
19901986

0 commit comments

Comments
 (0)