rustfs-scanner 1.0.0

RustFS Scanner provides scanning capabilities for data integrity checks, health monitoring, and storage analysis.
//! 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));
    }
}