diff --git a/crates/ecstore/src/bucket/lifecycle/bucket_lifecycle_ops.rs b/crates/ecstore/src/bucket/lifecycle/bucket_lifecycle_ops.rs index ec5aaf74e..326b798ee 100644 --- a/crates/ecstore/src/bucket/lifecycle/bucket_lifecycle_ops.rs +++ b/crates/ecstore/src/bucket/lifecycle/bucket_lifecycle_ops.rs @@ -1783,7 +1783,7 @@ impl TransitionState { .await; } global_metrics().record_scanner_transition_failed(1); - if !is_err_version_not_found(&err) && !is_err_object_not_found(&err) && !is_network_or_host_down(&err.to_string(), false) && !err.to_string().contains("use of closed network connection") { + if !is_err_version_not_found(&err) && !is_err_object_not_found(&err) && !is_network_or_host_down(&err.to_string(), false) { error!( event = EVENT_LIFECYCLE_TIER_OPERATION_FAILED, component = LOG_COMPONENT_ECSTORE, diff --git a/crates/ecstore/src/error/mod.rs b/crates/ecstore/src/error/mod.rs index 76dcc7a0c..f8215d908 100644 --- a/crates/ecstore/src/error/mod.rs +++ b/crates/ecstore/src/error/mod.rs @@ -1116,6 +1116,14 @@ mod tests { assert!(encoder_source.is::()); } + // The lifecycle transition worker relies on this arm alone to suppress the + // closed-connection noise (`bucket_lifecycle_ops.rs`); dropping it here would + // silently turn shutdown races back into `error!` log spam. + #[test] + fn is_network_or_host_down_covers_closed_network_connection() { + assert!(is_network_or_host_down("transition failed: use of closed network connection", false)); + } + // Regression for #952 (ECA-11): an all-`DiskNotFound` slice (every drive in // every set unreachable) must NOT be classified as "all not found", // otherwise ListObjects silently returns an empty listing and masks a full diff --git a/rustfs/src/admin/handlers/kms_dynamic.rs b/rustfs/src/admin/handlers/kms_dynamic.rs index 887ec7fad..d7ee7a0f7 100644 --- a/rustfs/src/admin/handlers/kms_dynamic.rs +++ b/rustfs/src/admin/handlers/kms_dynamic.rs @@ -22,6 +22,7 @@ use crate::admin::runtime_sources::{ current_object_store_handle_for_context, current_or_init_kms_runtime_service_manager, }; use crate::admin::storage_api::config::{read_admin_config, save_admin_config}; +use crate::admin::storage_api::error::StorageError; use crate::auth::{check_key_valid, get_session_token}; use crate::server::{ADMIN_PREFIX, RemoteAddr}; use hyper::{Method, StatusCode}; @@ -278,8 +279,11 @@ pub async fn load_kms_config() -> Option { } }, Err(e) => { - // Config not found is normal on first run - if e.to_string().contains("ConfigNotFound") || e.to_string().contains("not found") { + // Config not found is normal on first run: `read_config` maps a missing or + // empty config object to `ConfigNotFound`, so that variant is the only + // "absent" signal reaching here. Every other not-found variant (disk, + // volume, bucket) means degraded storage and must stay a warning. + if matches!(e, StorageError::ConfigNotFound) { info!( component = LOG_COMPONENT_ADMIN, subsystem = LOG_SUBSYSTEM_KMS,