mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-11-03 18:47:41 +01:00
rust volume: count sorted-map files and deletes the way Go does (#12365)
* rust volume: count sorted-map files and deletes the way Go does The Rust sorted needle map still used the counter rule Go dropped when the index loaders were unified: every .idx row, tombstones included, counted as a file, and each delete added one file and two deletions. Go counts put rows as files and a delete as one deletion of the live row's bytes, so the Go and Rust copies of one volume reported different FileCount and DeleteCount for the same index. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * rust volume: test canDelete volume counters across restarts Mirrors the Go store test: a canDelete volume reloaded from .sdx must count its deletes, and the counters it reports must match what the next reload computes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5df7faf0e3
commit
5c99744e00
2 files changed
+106
-42
No files matched your search
@@ -165,7 +165,8 @@ impl SortedFileNeedleMap {
|
||||
return Ok(None);
|
||||
}
|
||||
let file = pooled_index_files().borrow(&self.db_file_name, true)?;
|
||||
let Some((entry_index, _, size)) = search_sorted_index(&file, self.db_file_size, key)?
|
||||
let Some((entry_index, needle_offset, size)) =
|
||||
search_sorted_index(&file, self.db_file_size, key)?
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
@@ -182,22 +183,14 @@ impl SortedFileNeedleMap {
|
||||
// already counts as deleted.
|
||||
self.append_to_index_file(key, offset, TOMBSTONE_FILE_SIZE)?;
|
||||
|
||||
// Move the counters to where a reload would put them, so the heartbeat
|
||||
// does not report this volume as garbage-free until it restarts. The
|
||||
// tombstone is one more .idx row, and under the rule index_metric
|
||||
// applies both it and the row it supersedes count as deletions; the
|
||||
// bytes leave the live set without leaving the .idx.
|
||||
//
|
||||
// This belongs to the `.idx` append, not to the `.sdx` mark below: the
|
||||
// append is what makes the delete durable and what a reload would
|
||||
// count, and the retry path returns early once the overlay records it.
|
||||
// Leaving the counters until after the mark would strand them at their
|
||||
// pre-delete values whenever the mark fails.
|
||||
self.metric.file_count.fetch_add(1, Ordering::Relaxed);
|
||||
self.metric.deletion_count.fetch_add(2, Ordering::Relaxed);
|
||||
self.metric
|
||||
.deletion_byte_count
|
||||
.fetch_add(size.0.max(0) as u64, Ordering::Relaxed);
|
||||
// Count against the `.idx` append, not the `.sdx` mark below: the
|
||||
// append is what a reload counts, and the retry path returns early
|
||||
// once the overlay records it, so a failed mark must not strand the
|
||||
// counters at their pre-delete values.
|
||||
self.metric.on_delete(&NeedleValue {
|
||||
offset: needle_offset,
|
||||
size,
|
||||
});
|
||||
|
||||
// From here the delete is durable in `.idx`, so no lookup may report
|
||||
// this needle live again. Record it before touching `.sdx` and clear
|
||||
@@ -449,11 +442,11 @@ fn write_sorted_file_from_idx(
|
||||
/// its bloom filter — and so without its false positives or its per-volume
|
||||
/// allocation.
|
||||
///
|
||||
/// Go counts every `.idx` row in `FileCounter`, and counts a row in
|
||||
/// `DeletionCounter` when it is not the last, still-valid row for its key. The
|
||||
/// rows that *are* the last valid row for their key are exactly the entries
|
||||
/// `.sdx` holds, so `DeletionCounter = rows - live` and
|
||||
/// `DeletionByteCounter = valid bytes in .idx - live bytes in .sdx`, each
|
||||
/// Go counts every `.idx` put row in `FileCounter` and tombstones in neither
|
||||
/// counter, so a key costs one deletion per put that is no longer its live
|
||||
/// row. The live rows are exactly the unmarked entries `.sdx` holds, so
|
||||
/// `DeletionCounter = puts - live` and
|
||||
/// `DeletionByteCounter = put bytes in .idx - live bytes in .sdx`, each
|
||||
/// computable in one sequential pass with no per-key state.
|
||||
fn index_metric(
|
||||
index_file_name: &str,
|
||||
@@ -462,15 +455,17 @@ fn index_metric(
|
||||
) -> io::Result<NeedleMapMetric> {
|
||||
let metric = NeedleMapMetric::default();
|
||||
|
||||
let mut rows: i64 = 0;
|
||||
let mut valid_bytes: u64 = 0;
|
||||
let is_put = |offset: Offset, size: Size| !offset.is_zero() && !size.is_deleted();
|
||||
|
||||
let mut puts: i64 = 0;
|
||||
let mut put_bytes: u64 = 0;
|
||||
let mut index_file = File::open(index_file_name)?;
|
||||
idx::walk_index_file(&mut index_file, 0, |key, offset, size| {
|
||||
rows += 1;
|
||||
metric.maybe_set_max_file_key(key);
|
||||
metric.maybe_set_max_needle_end(offset, size, version);
|
||||
if size.is_valid() {
|
||||
valid_bytes += size.0 as u64;
|
||||
if is_put(offset, size) {
|
||||
puts += 1;
|
||||
put_bytes += size.0 as u64;
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
@@ -478,22 +473,22 @@ fn index_metric(
|
||||
let mut live: i64 = 0;
|
||||
let mut live_bytes: u64 = 0;
|
||||
let mut db_file = File::open(db_file_name)?;
|
||||
idx::walk_index_file(&mut db_file, 0, |_, _, size| {
|
||||
if size.is_valid() {
|
||||
idx::walk_index_file(&mut db_file, 0, |_, offset, size| {
|
||||
if is_put(offset, size) {
|
||||
live += 1;
|
||||
live_bytes += size.0 as u64;
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
metric.file_count.store(rows, Ordering::Relaxed);
|
||||
metric.file_byte_count.store(valid_bytes, Ordering::Relaxed);
|
||||
metric.file_count.store(puts, Ordering::Relaxed);
|
||||
metric.file_byte_count.store(put_bytes, Ordering::Relaxed);
|
||||
metric
|
||||
.deletion_count
|
||||
.store((rows - live).max(0), Ordering::Relaxed);
|
||||
.store((puts - live).max(0), Ordering::Relaxed);
|
||||
metric
|
||||
.deletion_byte_count
|
||||
.store(valid_bytes.saturating_sub(live_bytes), Ordering::Relaxed);
|
||||
.store(put_bytes.saturating_sub(live_bytes), Ordering::Relaxed);
|
||||
Ok(metric)
|
||||
}
|
||||
|
||||
@@ -617,11 +612,11 @@ mod tests {
|
||||
);
|
||||
|
||||
let m = SortedFileNeedleMap::open(&base, version()).unwrap();
|
||||
// Every row counts, matching Go's FileCounter.
|
||||
assert_eq!(m.file_count(), 5);
|
||||
// Every put row counts, matching Go's FileCounter; the tombstone does not.
|
||||
assert_eq!(m.file_count(), 4);
|
||||
assert_eq!(m.content_size(), 100 + 200 + 150 + 300);
|
||||
// Superseded row for key 1, plus both rows for the deleted key 2.
|
||||
assert_eq!(m.deleted_count(), 3);
|
||||
// Superseded row for key 1, plus the put of the deleted key 2.
|
||||
assert_eq!(m.deleted_count(), 2);
|
||||
assert_eq!(m.deleted_size(), 100 + 200);
|
||||
assert_eq!(m.max_file_key(), NeedleId(3));
|
||||
}
|
||||
@@ -709,8 +704,8 @@ mod tests {
|
||||
// already agree with it or the heartbeat jumps at every restart.
|
||||
drop(m);
|
||||
let reloaded = SortedFileNeedleMap::open(&base, version()).unwrap();
|
||||
assert_eq!(reloaded.file_count(), 4);
|
||||
assert_eq!(reloaded.deleted_count(), 2);
|
||||
assert_eq!(reloaded.file_count(), 3);
|
||||
assert_eq!(reloaded.deleted_count(), 1);
|
||||
assert_eq!(reloaded.deleted_size(), 200);
|
||||
assert_eq!(reloaded.content_size(), 600);
|
||||
|
||||
@@ -718,7 +713,7 @@ mod tests {
|
||||
reloaded
|
||||
.delete(NeedleId(2), Offset::from_actual_offset(16))
|
||||
.unwrap();
|
||||
assert_eq!((reloaded.file_count(), reloaded.deleted_count()), (4, 2));
|
||||
assert_eq!((reloaded.file_count(), reloaded.deleted_count()), (3, 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -960,7 +955,7 @@ mod tests {
|
||||
|
||||
assert_eq!(
|
||||
m.deleted_count(),
|
||||
deleted_before + 2,
|
||||
deleted_before + 1,
|
||||
"a durable tombstone must move the deletion count"
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -979,7 +974,7 @@ mod tests {
|
||||
.unwrap(),
|
||||
None
|
||||
);
|
||||
assert_eq!(m.deleted_count(), deleted_before + 2);
|
||||
assert_eq!(m.deleted_count(), deleted_before + 1);
|
||||
assert_eq!(m.deleted_size(), bytes_before + 100);
|
||||
}
|
||||
|
||||
|
||||
@@ -11430,6 +11430,75 @@ mod tests {
|
||||
v.write_needle(&mut n, true, false).unwrap();
|
||||
}
|
||||
|
||||
// After a restart a canDelete volume is served from .sdx; its deletes must
|
||||
// still move the counters volume.deleteEmpty and vacuum read, and land them
|
||||
// where the next reload would.
|
||||
#[test]
|
||||
fn test_read_only_can_delete_counts_deletes_after_restart() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let dir = tmp.path().to_str().unwrap();
|
||||
let ids = 1..=3u64;
|
||||
{
|
||||
let v = make_test_volume(dir);
|
||||
for id in ids.clone() {
|
||||
let mut n = Needle {
|
||||
id: NeedleId(id),
|
||||
cookie: Cookie(id as u32),
|
||||
data: b"payload".to_vec(),
|
||||
data_size: 7,
|
||||
..Needle::default()
|
||||
};
|
||||
v.write_needle(&mut n, true, false).unwrap();
|
||||
}
|
||||
v.set_read_only_persist(true, true).unwrap();
|
||||
v.close();
|
||||
}
|
||||
let reopen = || {
|
||||
Volume::new(
|
||||
dir,
|
||||
dir,
|
||||
VolumeId(1),
|
||||
NeedleMapKind::InMemory,
|
||||
&VolumeSpec::default(),
|
||||
)
|
||||
.unwrap()
|
||||
};
|
||||
let counters = |v: &Volume| {
|
||||
(
|
||||
v.file_count(),
|
||||
v.deleted_count(),
|
||||
v.content_size(),
|
||||
v.deleted_size(),
|
||||
)
|
||||
};
|
||||
|
||||
let v = reopen();
|
||||
assert!(matches!(
|
||||
v.io.read().unwrap().nm,
|
||||
Some(NeedleMap::SortedFile(_))
|
||||
));
|
||||
for id in ids.clone() {
|
||||
v.delete_needle(&mut Needle {
|
||||
id: NeedleId(id),
|
||||
cookie: Cookie(id as u32),
|
||||
..Needle::default()
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
let after_deletes = counters(&v);
|
||||
assert_eq!(after_deletes.0, 3);
|
||||
assert_eq!(
|
||||
after_deletes.0, after_deletes.1,
|
||||
"an emptied volume must report every file deleted"
|
||||
);
|
||||
assert_eq!(after_deletes.2, after_deletes.3);
|
||||
v.close();
|
||||
drop(v);
|
||||
|
||||
let v = reopen();
|
||||
assert_eq!(counters(&v), after_deletes);
|
||||
}
|
||||
|
||||
// Upgrading a volume that booted plain persisted-readonly to canDelete
|
||||
// must attach the missing .idx writer, or delete tombstones cannot append.
|
||||
#[test]
|
||||
|
||||
Reference in new issue
Block a user