volume: report EC file and delete counts in incremental shard messages (#12404)

* seaweed-volume: report EC file and delete counts in heartbeat deltas

The full EC heartbeat already carries file_count and delete_count, but the
incremental new/deleted shard messages built in
collect_ec_shard_delta_messages left both at zero. A volume the master first
learns of through a delta then reads 0 files until the next full EC
heartbeat. Fill both from file_and_delete_count, the same source the full
heartbeat uses, computed once per volume rather than once per shard.

Claude-Session: https://claude.ai/code/session_011UpvafcfUuC2mLxNAXLQJi

* volume: report EC file and delete counts in mount notifications

Go parity with the Rust delta change: the NewEcShardsChan messages sent on
MountEcShards and on startup shard load now carry FileCount and DeleteCount
from EcVolume.FileAndDeleteCount, the same values the full EC heartbeat
reports, instead of leaving them at zero.

Claude-Session: https://claude.ai/code/session_011UpvafcfUuC2mLxNAXLQJi

* seaweed-volume: leave count-only EC changes out of the shard delta

The delta messages now carry file_count and delete_count, so the whole-message
comparison re-sent every shard of a volume after any delete, and the master
broadcast each one as a new EC vid. Compare the shard fields only; the full EC
heartbeat already carries the counts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* topology: mount messages now carry EC counts, only older servers omit them

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* storage: guard the captured EC mount messages in the disk-type tests

The drain goroutine appended to the slice that waitForShardMsg polled with no
lock, which go test -race reports.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* storage: test EC counts on the startup notification and repeated .ecj ids

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* seaweed-volume: move file_count too in the count-only delta test

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:
Chris LuandClaude Opus 5.5 authored and GitHub committed 2026-11-04 10:43:04 -08:00
1 parent 3cbace2471
commit 3d411c6a6c
6 files changed
+216 -17

No files matched your search

+100 -1
View File
@@ -465,6 +465,9 @@ fn collect_ec_shard_delta_messages(
if quarantined {
continue;
}
// Same numbers the full EC heartbeat carries, read off the active
// generation's index and journal; one call covers every shard.
let (file_count, delete_count) = ec_vol.file_and_delete_count();
for shard in ec_vol.shards.iter().flatten() {
messages.insert(
(
@@ -481,6 +484,8 @@ fn collect_ec_shard_delta_messages(
disk_type: ec_vol.disk_type.to_string(),
expire_at_sec: ec_vol.expire_at_sec,
disk_id: disk_id as u32,
file_count,
delete_count,
encode_ts_ns: ec_vol.encode_ts_ns,
..Default::default()
},
@@ -503,7 +508,17 @@ fn diff_ec_shard_delta_messages(
let mut deleted_ec_shards = Vec::new();
for (key, message) in current {
if previous.get(key) != Some(message) {
// Counts move with every delete; the full EC heartbeat carries those,
// so only a change to the shard itself is news here.
let unchanged = previous.get(key).is_some_and(|prev| {
*message
== master_pb::VolumeEcShardInformationMessage {
file_count: message.file_count,
delete_count: message.delete_count,
..prev.clone()
}
});
if !unchanged {
new_ec_shards.push(message.clone());
}
}
@@ -3157,4 +3172,88 @@ mod tests {
assert_eq!(deleted_after_delete[0].ec_index_bits, 1);
assert_eq!(deleted_after_delete[0].shard_sizes, vec![0]);
}
#[test]
fn test_diff_ec_shard_delta_messages_ignores_count_changes() {
let key = (81, "ec_delta_case".to_string(), 0, 0);
let previous = HashMap::from([(
key.clone(),
master_pb::VolumeEcShardInformationMessage {
id: 81,
collection: "ec_delta_case".to_string(),
ec_index_bits: 1,
shard_sizes: vec![5],
file_count: 3,
..Default::default()
},
)]);
let mut current = previous.clone();
current.get_mut(&key).unwrap().file_count = 4;
current.get_mut(&key).unwrap().delete_count = 1;
let (new_ec_shards, deleted_ec_shards) = diff_ec_shard_delta_messages(&previous, &current);
assert!(new_ec_shards.is_empty());
assert!(deleted_ec_shards.is_empty());
current.get_mut(&key).unwrap().shard_sizes = vec![6];
let (new_ec_shards, _) = diff_ec_shard_delta_messages(&previous, &current);
assert_eq!(new_ec_shards.len(), 1);
assert_eq!(new_ec_shards[0].delete_count, 1);
}
/// Both EC emission paths report the counts the Go server's
/// `EcVolume.FileAndDeleteCount` defines: file_count is every entry in the
/// active .ecx (live and tombstoned alike), delete_count the needles this
/// holder deleted.
#[test]
fn test_ec_shard_messages_carry_file_and_delete_counts() {
use crate::storage::types::{NEEDLE_MAP_ENTRY_SIZE, Offset, Size, idx_entry_to_bytes};
let temp_dir = tempfile::tempdir().unwrap();
let dir = temp_dir.path().to_str().unwrap();
let mut store = Store::new(NeedleMapKind::InMemory);
store
.add_location(
dir,
dir,
8,
DiskType::HardDrive,
MinFreeSpace::Percent(1.0),
Vec::new(),
)
.unwrap();
std::fs::write(format!("{}/ec_counts_case_91.ec00", dir), b"shard").unwrap();
let mut ecx = Vec::new();
for (key, offset) in [(1u64, 8u32), (5, 200), (10, 500)] {
let mut entry = [0u8; NEEDLE_MAP_ENTRY_SIZE];
idx_entry_to_bytes(
&mut entry,
NeedleId(key),
Offset::from_actual_offset(offset as i64),
Size(100),
);
ecx.extend_from_slice(&entry);
}
std::fs::write(format!("{}/ec_counts_case_91.ecx", dir), &ecx).unwrap();
store.locations[0]
.mount_ec_shards(VolumeId(91), "ec_counts_case", &[0], "")
.unwrap();
let ec_vol = store.locations[0].find_ec_volume_mut(VolumeId(91)).unwrap();
ec_vol.journal_delete(NeedleId(1)).unwrap();
let delta = collect_ec_shard_delta_messages(&store);
let message = delta
.get(&(91, "ec_counts_case".to_string(), 0, 0))
.expect("delta message for the mounted shard");
assert_eq!(message.file_count, 3);
assert_eq!(message.delete_count, 1);
let heartbeat = collect_ec_heartbeat(&test_config(), &store);
assert_eq!(heartbeat.ec_shards.len(), 1);
assert_eq!(heartbeat.ec_shards[0].file_count, 3);
assert_eq!(heartbeat.ec_shards[0].delete_count, 1);
}
}