v0.13.9: Disc::copy stall guard + SgIoTransport no-reopen-on-timeout
Fixes the silent Pass 1 hang observed on Dune 2 with v0.13.8 (drive grinding through bad sectors at 0 KB/s, errs=0, no error surfaced). Root cause: SgIoTransport::execute's reopen-after-poll-timeout opened a fresh /dev/sg* fd on the main thread, which serialized against the spawned close() of the old fd via the kernel's per-device state lock. The userspace 1.5s timeout still fired, but the abandon-and-reopen recovery itself blocked the main thread for as long as close() took. Net: reads returned slowly, skip-forward fired on every iteration, bytes_good never advanced. - SgIoTransport::execute: on poll timeout, spawn close, set fd=-1, return Err. No reopen on the main thread. The transport is now invalidated until the consumer creates a fresh Drive. - Disc::copy: add stall guard. CopyOptions.stall_secs (default 120s). If bytes_good doesn't advance for the threshold, break 'outer cleanly with complete=false, bytes_pending > 0 so Pass 2 retries pick up the NonTrimmed ranges with recovery=true 30s timeouts. - New regression test: test_disc_copy_stall_detection_triggers_ skip_forward in tests/integration_progress_and_halt.rs.
This commit is contained in:
+26
-14
@@ -321,26 +321,38 @@ impl ScsiTransport for SgIoTransport {
|
||||
|
||||
if pr <= 0 {
|
||||
// Timeout (0) or fatal poll error (-1).
|
||||
// Command is still pending in the kernel. Abandon this fd and
|
||||
// open a fresh one. The old fd is closed in a background thread
|
||||
// because close() blocks until the kernel completes/aborts the
|
||||
// pending command.
|
||||
// Command is still pending in the kernel. Abandon the fd by
|
||||
// spawning a background close (which will block until the
|
||||
// kernel completes/aborts the pending command), and mark
|
||||
// this transport invalid by setting `self.fd = -1`.
|
||||
//
|
||||
// Why we no longer reopen on the main thread: opening the
|
||||
// SAME /dev/sg* device while the prior fd is mid-close
|
||||
// serializes via the kernel's per-device state lock, so
|
||||
// `libc::open()` on the main thread blocks for the same
|
||||
// duration that close() does — defeating the userspace
|
||||
// timeout. Observed in v0.13.8 live test on Dune 2: each
|
||||
// timed-out read added 60+ s to the next iteration of
|
||||
// Disc::copy, leaving the rip stuck without surfacing an
|
||||
// error or wedging the drive.
|
||||
//
|
||||
// Net effect of the fix: a single read timeout invalidates
|
||||
// the SgIoTransport. The Drive is now "dead" until the
|
||||
// consumer (autorip's rip thread) catches the failure and
|
||||
// reopens. Disc::copy's `skip_on_error=true` path will see
|
||||
// the Err and skip-forward, advancing pos, and the next
|
||||
// read on this fd returns Err(DeviceNotFound) immediately —
|
||||
// which Disc::copy continues to skip-forward through until
|
||||
// the NonTried region is exhausted. Pass 1 then ends with
|
||||
// bytes_pending > 0 and the rip thread reopens the Drive
|
||||
// for Pass 2 (Disc::patch with recovery=true and 30 s
|
||||
// timeouts).
|
||||
let old_fd = self.fd;
|
||||
self.fd = -1;
|
||||
|
||||
std::thread::spawn(move || {
|
||||
unsafe { libc::close(old_fd) };
|
||||
});
|
||||
|
||||
let c_path = Self::to_c_path(&self.device_path);
|
||||
let new_fd = unsafe {
|
||||
libc::open(
|
||||
c_path.as_ptr() as *const libc::c_char,
|
||||
libc::O_RDWR | libc::O_NONBLOCK | libc::O_CLOEXEC,
|
||||
)
|
||||
};
|
||||
self.fd = if new_fd >= 0 { new_fd } else { -1 };
|
||||
|
||||
return Err(Error::ScsiError {
|
||||
opcode: cdb[0],
|
||||
status: 0xFF,
|
||||
|
||||
Reference in New Issue
Block a user