From 1c5c28842a488fe98745151cd86ba3bdaddff1cf Mon Sep 17 00:00:00 2001 From: cxymds Date: Mon, 24 Aug 2026 14:17:11 +0800 Subject: [PATCH] fix(scanner): reject duplicate usage updates (#6445) * fix(scanner): reject duplicate usage updates * style: format decommission test imports * fix(ci): remove unused decommission and healing facades * fix(ci): cfg-gate test-only usage overlay import --------- Co-authored-by: houseme --- crates/scanner/src/scanner_io/cache.rs | 37 +++++++++++++++++--------- crates/scanner/src/scanner_io/tests.rs | 30 +++++++++++++++++++++ 2 files changed, 55 insertions(+), 12 deletions(-) diff --git a/crates/scanner/src/scanner_io/cache.rs b/crates/scanner/src/scanner_io/cache.rs index 7b12d7932..9522ffed2 100644 --- a/crates/scanner/src/scanner_io/cache.rs +++ b/crates/scanner/src/scanner_io/cache.rs @@ -751,16 +751,29 @@ pub(super) async fn persist_and_publish_cache_snapshot( } pub(super) async fn send_data_usage_update(updates: &mpsc::Sender, data_usage_info: DataUsageInfo) -> Result<()> { - updates.send(data_usage_info).await.map_err(|e| { - error!( - target: "rustfs::scanner::io", - event = EVENT_SCANNER_DATA_USAGE_STREAM, - component = LOG_COMPONENT_SCANNER, - subsystem = LOG_SUBSYSTEM_IO, - state = "send_failed", - error = %e, - "Scanner data usage publish failed" - ); - StorageError::other("scanner data usage receiver closed before update delivery") - }) + match updates.try_send(data_usage_info) { + Ok(()) => Ok(()), + Err(mpsc::error::TrySendError::Closed(_)) => { + error!( + target: "rustfs::scanner::io", + event = EVENT_SCANNER_DATA_USAGE_STREAM, + component = LOG_COMPONENT_SCANNER, + subsystem = LOG_SUBSYSTEM_IO, + state = "send_failed", + "Scanner data usage publish failed because the receiver is closed" + ); + Err(StorageError::other("scanner data usage receiver closed before update delivery")) + } + Err(mpsc::error::TrySendError::Full(_)) => { + error!( + target: "rustfs::scanner::io", + event = EVENT_SCANNER_DATA_USAGE_STREAM, + component = LOG_COMPONENT_SCANNER, + subsystem = LOG_SUBSYSTEM_IO, + state = "send_would_block", + "Scanner data usage publish rejected because an update is already queued" + ); + Err(StorageError::other("scanner data usage update already queued")) + } + } } diff --git a/crates/scanner/src/scanner_io/tests.rs b/crates/scanner/src/scanner_io/tests.rs index 8758eaf5e..6dd73ebe5 100644 --- a/crates/scanner/src/scanner_io/tests.rs +++ b/crates/scanner/src/scanner_io/tests.rs @@ -268,6 +268,36 @@ async fn data_usage_publish_fails_when_receiver_is_closed() { assert!(err.to_string().contains("receiver closed")); } +#[tokio::test] +async fn data_usage_publish_rejects_a_second_terminal_update_without_blocking() { + for (status, data_usage_info) in [ + (ScannerCycleStatus::Complete, DataUsageInfo::default()), + ( + ScannerCycleStatus::Superseded, + DataUsageInfo { + scanner_cycle: Some(7), + ..Default::default() + }, + ), + ] { + let (updates, mut receiver) = mpsc::channel(1); + assert!( + publish_usage_snapshot(&updates, status, data_usage_info) + .await + .expect("first terminal update should be accepted") + ); + + let err = + tokio::time::timeout(Duration::from_secs(1), publish_usage_snapshot(&updates, status, DataUsageInfo::default())) + .await + .expect("a full terminal update must fail without waiting") + .expect_err("a second terminal update must be rejected"); + assert!(err.to_string().contains("already queued")); + assert!(receiver.try_recv().is_ok(), "the first terminal update must remain owned by the receiver"); + assert!(receiver.try_recv().is_err(), "the rejected second update must not enter the channel"); + } +} + #[tokio::test] async fn multi_pool_scanner_cycle_publishes_combined_usage() { let (_temp_dir, store) = setup_two_pool_scanner_store().await;