mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-31 09:18:28 +00:00
feat(heal): disk-walk UNION enumeration to heal sub-quorum versions (backlog#920) (#4527)
B5 switched heal enumeration to list_object_versions, which only reflects the read-quorum metadata view: a version present on fewer than read-quorum disks was never enumerated, so it was never healed. Add a per-erasure-set disk-walk UNION enumerator (mirrors MinIO global-heal.go objQuorum=1 listPathRaw + mergeXLV2Versions) that surfaces every (object, version) present on ANY disk and feeds each to the existing per-version heal_object. - filemeta: MetaCacheEntries::resolve_union (dir_quorum=1/obj_quorum=1) yields the cross-disk version union at one tested seam. - ecstore: SetDisks::heal_walk_versions_page (list_path_raw fan-out, min_disks=1, dual object/version page bound, inclusive-forward de-overlap) + ECStore delegator + HealWalkVersion. - ecstore data-safety guard: before dangling-delete, try_regenerate_recoverable_meta physically probes part files via check_parts; when >= data_blocks data shards survive (meta lost but data recoverable) it regenerates xl.meta from a surviving FileInfo with the correct per-disk shard index instead of dangling-deleting. Genuine torn writes (< data_blocks) keep the current behavior — no resurrection. - heal: dw1: forward-marker cursor codec (reuses ResumeState.resume_cursor, idempotent restart on foreign tokens); list_versions_for_heal_page_disk_walk trait method (default falls back to the B5 read-quorum path); heal_bucket_with_resume selects the disk-walk enumerator when scan_mode==Deep || source==AutoHeal, else the unchanged B5 path; anti-loop guard aborts on (empty && truncated). Closes rustfs/backlog#920
This commit is contained in:
@@ -4018,6 +4018,69 @@ mod tests {
|
||||
assert_eq!(merged, vec![valid]);
|
||||
}
|
||||
|
||||
/// Build a single distinct Object version header for the union tests.
|
||||
fn union_version(version_u128: u128, mod_unix: i64) -> FileMetaShallowVersion {
|
||||
FileMetaShallowVersion {
|
||||
header: FileMetaVersionHeader {
|
||||
version_id: Some(Uuid::from_u128(version_u128)),
|
||||
mod_time: Some(OffsetDateTime::from_unix_timestamp(mod_unix).expect("valid timestamp")),
|
||||
signature: [0x11, 0x22, 0x33, 0x44],
|
||||
version_type: VersionType::Object,
|
||||
flags: 0,
|
||||
ec_n: 4,
|
||||
ec_m: 2,
|
||||
},
|
||||
meta: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// backlog#920: quorum==1 (union) must surface EVERY version present on ANY
|
||||
/// per-disk stream, even when the per-disk version sets are fully DISJOINT
|
||||
/// (a version living on a single disk). This is the enumeration guarantee the
|
||||
/// sub-quorum disk-walk depends on.
|
||||
#[test]
|
||||
fn merge_union_quorum1_yields_full_union_over_disjoint_disks() {
|
||||
let a = union_version(0xA, 1_705_312_300);
|
||||
let b = union_version(0xB, 1_705_312_200);
|
||||
let c = union_version(0xC, 1_705_312_100);
|
||||
|
||||
// Three disks, each holding exactly ONE distinct version (disjoint sets).
|
||||
let disks = [vec![a], vec![b], vec![c]];
|
||||
let merged = merge_file_meta_versions(1, true, 0, &disks);
|
||||
|
||||
let ids: std::collections::HashSet<Option<Uuid>> = merged.iter().map(|v| v.header.version_id).collect();
|
||||
assert_eq!(merged.len(), 3, "union must retain all three disjoint versions: {merged:?}");
|
||||
assert!(ids.contains(&Some(Uuid::from_u128(0xA))));
|
||||
assert!(ids.contains(&Some(Uuid::from_u128(0xB))));
|
||||
assert!(ids.contains(&Some(Uuid::from_u128(0xC))));
|
||||
|
||||
// Contrast: read-quorum (2) over the same disjoint streams surfaces NONE,
|
||||
// because no version reaches two agreeing disks. This is the exact gap.
|
||||
let read_quorum = merge_file_meta_versions(2, true, 0, &disks);
|
||||
assert!(
|
||||
read_quorum.is_empty(),
|
||||
"read-quorum merge must drop every sub-quorum version, got {read_quorum:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// The equal-modTime / distinct-versionId retain-and-advance branch: two
|
||||
/// versions sharing a mod_time but with different ids must BOTH survive the
|
||||
/// union merge (they are not collapsed as duplicates).
|
||||
#[test]
|
||||
fn merge_union_quorum1_retains_equal_modtime_distinct_version_ids() {
|
||||
let same_time = 1_705_312_300;
|
||||
let a = union_version(0xAA, same_time);
|
||||
let b = union_version(0xBB, same_time);
|
||||
|
||||
let disks = [vec![a], vec![b]];
|
||||
let merged = merge_file_meta_versions(1, true, 0, &disks);
|
||||
|
||||
let ids: std::collections::HashSet<Option<Uuid>> = merged.iter().map(|v| v.header.version_id).collect();
|
||||
assert_eq!(merged.len(), 2, "equal-modTime distinct-id versions must both survive: {merged:?}");
|
||||
assert!(ids.contains(&Some(Uuid::from_u128(0xAA))));
|
||||
assert!(ids.contains(&Some(Uuid::from_u128(0xBB))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn meta_object_init_free_version_rejects_invalid_tier_free_version_id() {
|
||||
let mut sys = HashMap::new();
|
||||
|
||||
Reference in New Issue
Block a user