Files
rustfs/crates/scanner/src/scanner_heal_admission_baseline.rs
T
Zhengchao An 7cac528de3 refactor(protos): move compat manifest send-site assertions into owning crates (#6618)
refactor(protos): move internode compat manifest send-site assertions into owning crates

Promotes the rolling-upgrade dual-write manifest from a test-only constant in rustfs-protos into the public rustfs_protos::compat_manifest module, moves the JSON-encoder send-site assertions into the crates that own the asserted sources (ecstore remote_disk.rs for requests, the rustfs binary node_service/disk.rs for responses), and splits the scanner Phase-0 overlap inventory so its heal- and ecstore-owned halves live in those crates. Adds a cross-crate include_str!/include! guard with fixture self-tests to scripts/check_layer_dependencies.sh so a library crate can never again read another crate's Rust source at compile time, and records the rule in docs/architecture/crate-boundaries.md.

Part of rustfs/backlog#1884.
2026-08-26 11:11:16 +08:00

180 lines
6.3 KiB
Rust

//! Executable Phase-0 contract for the scanner/heal overlap investigation.
//!
//! These tests model the matrix that a future storage-owned admission
//! primitive must satisfy. They intentionally do not provide a production
//! lock or coordinator; the issue's current evidence establishes a baseline,
//! not a demonstrated stale-writer failure.
#[cfg(test)]
mod tests {
// The heal- and ecstore-owned halves of the Phase-0 overlap inventory live
// with their owning crates (crates/heal/tests and crates/ecstore/tests):
// cross-crate source includes are rejected by
// scripts/check_layer_dependencies.sh.
const SCANNER_IO_SOURCE: &str = include_str!("scanner_io/io_disk.rs");
const SCANNER_FOLDER_SOURCE: &str = include_str!("scanner_folder.rs");
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Operation {
ScannerRead,
HealRead,
HealWrite,
DataMovementWrite,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct BaselineSample {
set: &'static str,
operation: Operation,
latency_us: u64,
backlog_depth: usize,
deferred: bool,
}
fn p99_latency(samples: &[BaselineSample]) -> u64 {
assert!(!samples.is_empty());
let mut latencies = samples.iter().map(|sample| sample.latency_us).collect::<Vec<_>>();
latencies.sort_unstable();
let rank = (latencies.len() * 99).div_ceil(100).saturating_sub(1);
latencies[rank]
}
fn restart_degraded_fixture() -> [BaselineSample; 8] {
[
BaselineSample {
set: "pool0/set0",
operation: Operation::ScannerRead,
latency_us: 120,
backlog_depth: 1,
deferred: false,
},
BaselineSample {
set: "pool0/set0",
operation: Operation::HealRead,
latency_us: 180,
backlog_depth: 1,
deferred: false,
},
BaselineSample {
set: "pool0/set0",
operation: Operation::HealWrite,
latency_us: 420,
backlog_depth: 2,
deferred: true,
},
BaselineSample {
set: "pool0/set0",
operation: Operation::ScannerRead,
latency_us: 160,
backlog_depth: 2,
deferred: false,
},
BaselineSample {
set: "pool0/set1",
operation: Operation::ScannerRead,
latency_us: 110,
backlog_depth: 0,
deferred: false,
},
BaselineSample {
set: "pool0/set1",
operation: Operation::HealRead,
latency_us: 150,
backlog_depth: 0,
deferred: false,
},
BaselineSample {
set: "pool0/set1",
operation: Operation::HealWrite,
latency_us: 360,
backlog_depth: 1,
deferred: true,
},
BaselineSample {
set: "pool0/set1",
operation: Operation::ScannerRead,
latency_us: 130,
backlog_depth: 1,
deferred: false,
},
]
}
fn same_set(a: &str, b: &str) -> bool {
a == b
}
fn may_overlap(left: Operation, right: Operation, same_set: bool) -> bool {
if !same_set {
return true;
}
matches!(
(left, right),
(Operation::ScannerRead, Operation::HealRead) | (Operation::HealRead, Operation::ScannerRead)
)
}
#[test]
fn scanner_heal_matrix_allows_read_read_and_blocks_heal_write() {
assert!(may_overlap(Operation::ScannerRead, Operation::HealRead, true));
assert!(!may_overlap(Operation::ScannerRead, Operation::HealWrite, true));
assert!(!may_overlap(Operation::DataMovementWrite, Operation::HealRead, true));
}
#[test]
fn scanner_heal_different_sets_remain_concurrent() {
assert!(may_overlap(
Operation::HealWrite,
Operation::ScannerRead,
same_set("pool0/set0", "pool0/set1")
));
}
#[test]
fn scanner_heal_restart_and_clock_skew_do_not_accept_old_owner() {
let old_owner_generation = 3_u64;
let restarted_generation = 4_u64;
let persisted_timestamp = 100_u64;
let observed_timestamp = 90_u64;
assert_ne!(old_owner_generation, restarted_generation);
assert!(observed_timestamp < persisted_timestamp);
}
#[test]
fn scanner_heal_overlap_inventory_has_no_unprotected_destructive_entry() {
// Keep the Phase-0 inventory tied to real entry points. The assertions
// deliberately check that the documented guards still exist; they do
// not claim that a shared admission primitive already exists.
assert!(SCANNER_IO_SOURCE.contains("let _guard = self.start_scan()"));
assert!(SCANNER_IO_SOURCE.contains("scan_data_folder"));
assert!(SCANNER_FOLDER_SOURCE.contains("send_required_scanner_heal_request"));
assert!(SCANNER_FOLDER_SOURCE.contains("update_pending_scanner_heal_after_admission"));
}
#[test]
fn scanner_heal_admission_benchmark_degraded_quorum() {
let samples = restart_degraded_fixture();
assert_eq!(p99_latency(&samples), 420);
assert!(
samples
.iter()
.any(|sample| sample.operation == Operation::HealWrite && sample.deferred)
);
assert!(samples.iter().any(|sample| sample.set == "pool0/set1" && !sample.deferred));
assert_eq!(samples.iter().map(|sample| sample.backlog_depth).max(), Some(2));
}
#[test]
fn scanner_heal_set_deferral_preserves_quorum_and_backlog() {
let samples = restart_degraded_fixture();
let deferred_count = samples.iter().filter(|sample| sample.deferred).count();
let independent_progress = samples
.iter()
.filter(|sample| sample.set == "pool0/set1" && !sample.deferred)
.count();
assert_eq!(deferred_count, 2);
assert_eq!(independent_progress, 3);
assert!(samples.iter().all(|sample| sample.backlog_depth <= 2));
}
}