New disc/read_error.rs as the single entry point all read failures flow through. Handler classifies the error, updates the in-flight context (damage window, retry budgets, jump multiplier), and returns a ReadAction the caller dispatches on. Pass 1 (sweep) refactored to use it; ~340 lines of nested if/else collapsed into ~120 lines of action dispatch. Adding a new error class = one match arm. Logging is in one place. Bisect inner failures don't poison the damage window. Jump multiplier capped at 64 (max 1 GB jump for batch=32 — observed prior unbounded behavior produce a single 56 GB jump on a wedged drive). Pass N (patch) damage_skip is now size-aware: each skip is capped at range_remaining/4 rather than the absolute MB-scale escalation. The old logic could leap over a 100-sector bad range that hides a 50-sector good middle; size-aware convergence finds the good middles instead. Tests in tests/pass_n_size_aware_skip.rs exercise the size-aware skip against synthetic patterns (25-bad/50-good/25-bad and three good middles in a row) and prove ≥98% of good middles are recovered. Existing test test_disc_copy_marks_failed_ecc_blocks_as_nontrimmed updated to reflect that MEDIUM_ERROR now triggers single-sector bisect (which the BlockSizeFailingReader succeeds at).
115 lines
4.1 KiB
Rust
115 lines
4.1 KiB
Rust
//! Tests for SgIoTransport timeout recovery (Fix 2).
|
|
//!
|
|
//! When `execute()` detects a transport-level failure (kernel timeout /
|
|
//! USB bridge wedge — `host_status != 0`), it spawns two background
|
|
//! threads to (1) close the old fd and (2) open a fresh one, then
|
|
//! stores the new fd in `fd_recovery`. The next `execute()` call picks
|
|
//! up the recovered fd without blocking on close().
|
|
//!
|
|
//! These tests require a real /dev/sg* device and are therefore #[ignore].
|
|
|
|
use std::path::Path;
|
|
|
|
#[test]
|
|
#[ignore]
|
|
fn test_sgio_transport_timeout_does_not_kill_transport() {
|
|
let device = "/dev/sg2";
|
|
let device = std::env::var("FREEMKV_TEST_SG_DEVICE").unwrap_or(device.to_string());
|
|
let _path = Path::new(&device);
|
|
|
|
#[cfg(target_os = "linux")]
|
|
{
|
|
use libfreemkv::scsi::ScsiTransport;
|
|
use libfreemkv::scsi::linux::SgIoTransport;
|
|
use libfreemkv::scsi::{DataDirection, SCSI_STATUS_TRANSPORT_FAILURE};
|
|
use std::sync::Arc;
|
|
use std::sync::atomic::{AtomicI32, Ordering};
|
|
|
|
let mut transport = SgIoTransport::open(_path).expect("open device");
|
|
let fd_before = transport.fd;
|
|
|
|
// READ_10 with 1 ms timeout to force kernel timeout.
|
|
let cdb = [0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00];
|
|
let mut data = vec![0u8; 2048];
|
|
|
|
let start = std::time::Instant::now();
|
|
let result = transport.execute(&cdb, DataDirection::FromDevice, &mut data, 1);
|
|
let _elapsed = start.elapsed();
|
|
|
|
assert!(result.is_err(), "expected Err on timeout, got {:?}", result);
|
|
let err = result.unwrap_err();
|
|
assert!(
|
|
matches!(&err, libfreemkv::Error::ScsiError { status, .. } if *status == SCSI_STATUS_TRANSPORT_FAILURE),
|
|
"expected ScsiError(TRANSPORT_FAILURE), got {:?}",
|
|
err
|
|
);
|
|
assert_eq!(transport.fd, -1, "fd should be -1 after timeout");
|
|
|
|
// Wait for recovery thread to store new fd.
|
|
let recovered = Arc::new(AtomicI32::new(-1));
|
|
let recovery_ref = transport.fd_recovery.clone();
|
|
for _ in 0..100 {
|
|
let v = recovery_ref.load(Ordering::Acquire);
|
|
if v >= 0 {
|
|
recovered.store(v, Ordering::Release);
|
|
break;
|
|
}
|
|
std::thread::sleep(Duration::from_millis(50));
|
|
}
|
|
let new_fd = recovered.load(Ordering::Acquire);
|
|
assert!(new_fd >= 0, "recovery thread should have produced a new fd");
|
|
|
|
// Next execute() must pick up recovered fd quickly.
|
|
let mut data2 = vec![0u8; 2048];
|
|
let start2 = std::time::Instant::now();
|
|
let result2 = transport.execute(&cdb, DataDirection::FromDevice, &mut data2, 5000);
|
|
let elapsed2 = start2.elapsed();
|
|
|
|
assert!(
|
|
result2.is_ok(),
|
|
"execute() with recovered fd should succeed, got {:?}",
|
|
result2
|
|
);
|
|
assert!(
|
|
elapsed2 < Duration::from_secs(2),
|
|
"should return quickly, took {:?}",
|
|
elapsed2
|
|
);
|
|
assert_ne!(transport.fd, -1, "fd should be valid after recovery");
|
|
assert_ne!(transport.fd, fd_before, "fd should be fresh after recovery");
|
|
}
|
|
#[cfg(not(target_os = "linux"))]
|
|
{
|
|
eprintln!("SKIP: test requires Linux / SgIoTransport");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
#[ignore]
|
|
fn test_drive_read_per_cdb_timeout_bounds_call() {
|
|
let device = "/dev/sg2";
|
|
let device = std::env::var("FREEMKV_TEST_SG_DEVICE").unwrap_or(device.to_string());
|
|
let _path = Path::new(&device);
|
|
|
|
#[cfg(target_os = "linux")]
|
|
{
|
|
let mut drive = libfreemkv::Drive::open(_path).expect("open drive");
|
|
let timeout_ms: u32 = 5_000;
|
|
|
|
let start = std::time::Instant::now();
|
|
let _ = drive.read(0, 1, &mut [0u8; 2048], false);
|
|
let elapsed = start.elapsed();
|
|
|
|
let overhead = Duration::from_millis(500);
|
|
assert!(
|
|
elapsed < Duration::from_millis(timeout_ms as u64) + overhead,
|
|
"Drive::read should return within timeout_ms + overhead, took {:?}",
|
|
elapsed
|
|
);
|
|
}
|
|
#[cfg(not(target_os = "linux"))]
|
|
{
|
|
eprintln!("SKIP: test requires Linux / SgIoTransport");
|
|
}
|
|
}
|