refactor(ecstore): make store-to-disk error narrowing named and fallible (#6626)

refactor(ecstore): make store-to-disk error narrowing a named fallible operation

Backlog#1845 step 4. The blanket impl From<StorageError> for DiskError let ? silently push store-only errors (locks, buckets, quotas) across the disk boundary into DiskError::other, where the rendered message fragments reduce_errs quorum buckets. Same story for the blanket From<StorageError> for rustfs_filemeta::Error and its other() catch-all.

Both impls are replaced by named, fallible methods: StorageError::narrow_to_disk() and StorageError::narrow_to_filemeta(). Variants with an identity on the far side map across unchanged - including the two documented lossy collapses (SlowDown -> TooManyOpenFiles, StorageFull -> DiskFull) that the round-trip tests pin - and everything else returns Err(self) so the call site decides what crossing the boundary means. Removing the impls let the compiler enumerate every conversion site; the census that scoped this issue had found 5, the compiler found 33.

Call sites keep their existing behavior: the io identity bridge and the generic sites fold Err into the io-backed other() exactly as the old catch-all did (identity still recoverable by downcast), listing paths use one shared to_filemeta_err helper, and the two sites that relied on the SlowDown collapse now construct DiskError::TooManyOpenFiles directly so the loss is visible where it happens. No behavior change intended anywhere; the io::Error bridge itself is untouched by design.

Ref rustfs/backlog#1845
This commit is contained in:
Zhengchao An
2026-08-26 12:32:37 +08:00
committed by GitHub
parent 1590d9107b
commit aa56d4b847
11 changed files with 155 additions and 88 deletions
+3 -1
View File
@@ -678,7 +678,9 @@ mod tests {
fn embedded_tonic_status_is_recovered_across_error_conversions() {
// DiskError and StorageError share one wrapper, so a status keeps its
// typed classification whichever error it was converted into first.
let from_storage: DiskErrorType = crate::error::Error::from(tonic::Status::unavailable("peer gone")).into();
let from_storage: DiskErrorType = crate::error::Error::from(tonic::Status::unavailable("peer gone"))
.narrow_to_disk()
.expect("status-derived Io errors narrow through the bridge");
let DiskError::Io(io_err) = &from_storage else {
panic!("status-derived disk error should stay an Io error");
};
@@ -3593,13 +3593,23 @@ mod tests {
// reduce_errs groups Io errors by kind plus rendered message: peers failing the
// same operation must stay a single dominant error instead of one bucket per peer.
let per_peer_errs = (0..4)
.map(|_| Some(DiskError::from(peer_failure_without_details("load_bucket_metadata", Some("shared")))))
.map(|_| {
Some(
peer_failure_without_details("load_bucket_metadata", Some("shared"))
.narrow_to_disk()
.unwrap_or_else(DiskError::other),
)
})
.collect::<Vec<_>>();
let (count, dominant) = reduce_errs(&per_peer_errs, &[]);
assert_eq!(count, 4, "one shared failure must not split into per-peer buckets");
assert_eq!(
dominant,
Some(DiskError::from(peer_failure_without_details("load_bucket_metadata", Some("shared"))))
Some(
peer_failure_without_details("load_bucket_metadata", Some("shared"))
.narrow_to_disk()
.unwrap_or_else(DiskError::other)
)
);
assert_ne!(
@@ -775,9 +775,10 @@ fn try_acquire_bucket_heal_movement_guard<'a>(
};
// Do not queue a receiver behind a movement writer while its coordinator
// holds another node's read guard; failing fast breaks that cross-node cycle.
gate.try_read()
.map(Some)
.map_err(|_| crate::error::StorageError::SlowDown.into())
// `StorageError::SlowDown` has no disk-layer identity of its own: it
// collapses to `TooManyOpenFiles` at this boundary (see `narrow_to_disk`).
// Constructed directly so the loss stays explicit at the site.
gate.try_read().map(Some).map_err(|_| Error::TooManyOpenFiles)
}
async fn acquire_bucket_heal_write_guard<'a>(
@@ -787,7 +788,9 @@ async fn acquire_bucket_heal_write_guard<'a>(
return Ok(None);
};
let guard = gate.lock().await;
guard.ensure_write_safe("bucket heal cannot run while pool metadata requires recovery")?;
guard
.ensure_write_safe("bucket heal cannot run while pool metadata requires recovery")
.map_err(|e| e.narrow_to_disk().unwrap_or_else(Error::other))?;
Ok(Some(guard))
}
@@ -2366,7 +2369,11 @@ mod tests {
let err = try_acquire_bucket_heal_movement_guard(Some(&gate), false)
.expect_err("receiver must not wait behind a queued movement writer");
assert_eq!(err, crate::error::StorageError::SlowDown.into());
assert_eq!(
err,
Error::TooManyOpenFiles,
"SlowDown collapses to TooManyOpenFiles at the disk boundary"
);
assert!(
try_acquire_bucket_heal_movement_guard(Some(&gate), true)
.expect("coordinator-owned movement guard should be reused")