mirror of
https://github.com/rustfs/rustfs.git
synced 2026-09-03 02:38:12 +00:00
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:
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user