fix(scanner): require authoritative usage snapshots (#5333)

This commit is contained in:
Henry Guo
2026-07-28 07:29:57 +08:00
committed by GitHub
parent 5a768d3a44
commit d5df66ac4f
25 changed files with 1829 additions and 374 deletions
+8 -4
View File
@@ -35,10 +35,10 @@ use storage_api::owner::{
EcstoreTierConfig, EcstoreVersioningApi, HTTPPreconditions, HTTPRangeSpec, ObjectIO, ObjectOperations, ObjectToDelete,
ScannerReplicationHealObject, ScannerReplicationHealResult, ScannerReplicationQueueAdmission, ecstore_apply_expiry_rule,
ecstore_apply_transition_rule, ecstore_expiry_state_handle, ecstore_get_global_tier_config_mgr, ecstore_get_lifecycle_config,
ecstore_get_object_lock_config, ecstore_get_replication_config, ecstore_is_erasure, ecstore_is_erasure_sd,
ecstore_is_reserved_or_invalid_bucket, ecstore_list_path_raw, ecstore_object_opts_from_object_info,
ecstore_path2_bucket_object, ecstore_path2_bucket_object_with_base_path, ecstore_read_config,
ecstore_replace_bucket_usage_memory_from_info, ecstore_resolve_object_store_handle, ecstore_save_config,
ecstore_get_object_lock_config, ecstore_get_replication_config, ecstore_invalidate_data_usage_snapshot_cache,
ecstore_is_erasure, ecstore_is_erasure_sd, ecstore_is_reserved_or_invalid_bucket, ecstore_list_path_raw,
ecstore_object_opts_from_object_info, ecstore_path2_bucket_object, ecstore_path2_bucket_object_with_base_path,
ecstore_read_config, ecstore_replace_bucket_usage_memory_from_info, ecstore_resolve_object_store_handle, ecstore_save_config,
scanner_replication_config_for_lifecycle_eval,
};
#[cfg(test)]
@@ -462,6 +462,10 @@ pub(crate) async fn replace_bucket_usage_memory_from_info(data_usage_info: &rust
ecstore_replace_bucket_usage_memory_from_info(data_usage_info).await;
}
pub(crate) async fn invalidate_data_usage_snapshot_cache() {
ecstore_invalidate_data_usage_snapshot_cache().await;
}
pub trait ScannerObjectIO:
ObjectIO<
Error = EcstoreError,
+135 -90
View File
@@ -61,8 +61,9 @@ use crate::storage_api::scan::{
};
use crate::{
ECStore, EcstoreError, RUSTFS_META_BUCKET, ScannerLifecycleConfigExt as _, ScannerReplicationConfigExt as _,
get_lifecycle_config, get_replication_config, read_config, replace_bucket_usage_memory_from_info, save_config,
save_config_shared_with_preconditions, save_config_with_preconditions, scanner_is_erasure_sd,
get_lifecycle_config, get_replication_config, invalidate_data_usage_snapshot_cache, read_config,
replace_bucket_usage_memory_from_info, save_config, save_config_shared_with_preconditions, save_config_with_preconditions,
scanner_is_erasure_sd,
};
const LOG_COMPONENT_SCANNER: &str = "scanner";
@@ -985,7 +986,7 @@ fn initial_scanner_delay_for_startup(
}
fn data_usage_info_is_cold(info: &DataUsageInfo) -> bool {
info.last_update.is_none() || (info.buckets_usage.is_empty() && info.bucket_sizes.is_empty())
!info.is_complete_bucket_usage_snapshot()
}
async fn read_data_usage_config_for_startup(storeapi: &Arc<impl ScannerObjectIO>) -> Result<Option<Vec<u8>>, EcstoreError> {
@@ -1004,10 +1005,12 @@ async fn read_data_usage_config_for_startup(storeapi: &Arc<impl ScannerObjectIO>
}
}
match read_pair(storeapi, DATA_USAGE_OBJ_NAME_PATH.as_str()).await? {
Some(data) => Ok(Some(data)),
None => read_pair(storeapi, LEGACY_DATA_USAGE_OBJ_NAME_PATH.as_str()).await,
for path in [DATA_USAGE_OBJ_NAME_PATH.as_str(), LEGACY_DATA_USAGE_OBJ_NAME_PATH.as_str()] {
if let Some(data) = read_pair(storeapi, path).await? {
return Ok(Some(data));
}
}
Ok(None)
}
fn data_usage_backup_due(data_usage_info: &DataUsageInfo) -> bool {
@@ -3611,6 +3614,21 @@ async fn store_data_usage_in_backend_with_outcome_for_epoch_and_baseline(
data_usage_info.scanner_epoch = Some(leader_epoch);
}
if !data_usage_info.is_complete_bucket_usage_snapshot() {
error!(
target: "rustfs::scanner",
event = EVENT_SCANNER_PERSIST_STATE,
component = LOG_COMPONENT_SCANNER,
subsystem = LOG_SUBSYSTEM_RUNTIME,
path = %DATA_USAGE_OBJ_NAME_PATH.as_str(),
state = "reject_incomplete_snapshot",
"Scanner refused to persist an incomplete data usage snapshot"
);
global_metrics().record_scanner_usage_save_result(ScannerUsageSaveResult::Failed);
outcome = DataUsagePersistOutcome::Failed;
continue;
}
let data = match serde_json::to_vec(&data_usage_info) {
Ok(data) => data,
Err(e) => {
@@ -3767,16 +3785,19 @@ async fn store_data_usage_in_backend_with_outcome_for_epoch_and_baseline(
match save_outcome {
DataUsagePersistOutcome::Current => {
invalidate_data_usage_snapshot_cache().await;
global_metrics().record_scanner_usage_save_result(ScannerUsageSaveResult::SkippedStale);
outcome = DataUsagePersistOutcome::Current;
continue;
}
DataUsagePersistOutcome::AlreadyDurable => {
invalidate_data_usage_snapshot_cache().await;
replace_bucket_usage_memory_from_info(&data_usage_info).await;
global_metrics().record_scanner_usage_save_result(ScannerUsageSaveResult::Success);
outcome = DataUsagePersistOutcome::AlreadyDurable;
}
DataUsagePersistOutcome::PriorCycleDurable => {
invalidate_data_usage_snapshot_cache().await;
global_metrics().record_scanner_usage_save_result(ScannerUsageSaveResult::Success);
outcome = DataUsagePersistOutcome::PriorCycleDurable;
}
@@ -3786,6 +3807,7 @@ async fn store_data_usage_in_backend_with_outcome_for_epoch_and_baseline(
continue;
}
DataUsagePersistOutcome::Saved => {
invalidate_data_usage_snapshot_cache().await;
replace_bucket_usage_memory_from_info(&data_usage_info).await;
global_metrics().record_scanner_usage_save_result(ScannerUsageSaveResult::Success);
outcome = DataUsagePersistOutcome::Saved;
@@ -4485,8 +4507,25 @@ mod tests {
assert_eq!(epoch, 11);
}
#[test]
fn scanner_startup_treats_incomplete_usage_snapshot_as_cold() {
let mut legacy = complete_usage_with_bucket_count(Some(std::time::SystemTime::now()), 1);
legacy.usage_snapshot_complete = false;
assert!(data_usage_info_is_cold(&legacy));
assert!(!data_usage_info_is_cold(&complete_usage_with_bucket_count(
Some(std::time::SystemTime::now()),
1,
)));
assert!(!data_usage_info_is_cold(&DataUsageInfo {
last_update: Some(std::time::SystemTime::now()),
usage_snapshot_complete: true,
..Default::default()
}));
}
#[tokio::test]
async fn scanner_startup_migrates_legacy_usage_only_until_v2_exists() {
async fn scanner_startup_prefers_v2_over_legacy_usage() {
let store = Arc::new(MemoryConfigStore::default());
let legacy = DataUsageInfo {
scanner_epoch: Some(19),
@@ -4517,9 +4556,10 @@ mod tests {
);
let authoritative = DataUsageInfo {
scanner_epoch: Some(7),
scanner_cycle: Some(11),
scanner_epoch: Some(23),
scanner_cycle: Some(51),
last_update: Some(std::time::SystemTime::now()),
usage_snapshot_complete: true,
..Default::default()
};
let authoritative_data = serde_json::to_vec(&authoritative).expect("v2 usage snapshot should encode");
@@ -4539,8 +4579,8 @@ mod tests {
.await
.expect("v2 usage floor should be authoritative"),
PersistedUsageFloor {
next_cycle: 12,
leader_epoch: 7,
next_cycle: 52,
leader_epoch: 23,
}
);
@@ -4594,7 +4634,7 @@ mod tests {
scanner_epoch: Some(1),
scanner_cycle: Some(cycle),
last_update: Some(std::time::SystemTime::now()),
..Default::default()
..complete_usage_with_bucket_count(None, 0)
})
.await
.expect("usage update should queue");
@@ -4959,16 +4999,8 @@ mod tests {
let (sender, receiver) = mpsc::channel(2);
let ctx = CancellationToken::new();
let newer = DataUsageInfo {
last_update: Some(std::time::SystemTime::UNIX_EPOCH + Duration::from_secs(20)),
buckets_count: 2,
..Default::default()
};
let older = DataUsageInfo {
last_update: Some(std::time::SystemTime::UNIX_EPOCH + Duration::from_secs(10)),
buckets_count: 1,
..Default::default()
};
let newer = complete_usage_with_bucket_count(Some(std::time::SystemTime::UNIX_EPOCH + Duration::from_secs(20)), 2);
let older = complete_usage_with_bucket_count(Some(std::time::SystemTime::UNIX_EPOCH + Duration::from_secs(10)), 1);
sender.send(newer).await.expect("newer usage snapshot should enqueue");
sender.send(older).await.expect("older usage snapshot should enqueue");
@@ -4993,16 +5025,8 @@ mod tests {
let (sender, receiver) = mpsc::channel(1);
let ctx = CancellationToken::new();
let key = memory_config_key(RUSTFS_META_BUCKET, DATA_USAGE_OBJ_NAME_PATH.as_str());
let newer = DataUsageInfo {
last_update: Some(std::time::SystemTime::UNIX_EPOCH + Duration::from_secs(20)),
buckets_count: 2,
..Default::default()
};
let stale = DataUsageInfo {
last_update: Some(std::time::SystemTime::UNIX_EPOCH + Duration::from_secs(10)),
buckets_count: 1,
..Default::default()
};
let newer = complete_usage_with_bucket_count(Some(std::time::SystemTime::UNIX_EPOCH + Duration::from_secs(20)), 2);
let stale = complete_usage_with_bucket_count(Some(std::time::SystemTime::UNIX_EPOCH + Duration::from_secs(10)), 1);
store
.interleaving_puts
.lock()
@@ -5045,6 +5069,7 @@ mod tests {
initial.bucket_sizes.insert("bucket-a".to_string(), 84);
initial.buckets_count = 1;
initial.calculate_totals();
mark_usage_snapshot_complete(&mut initial);
let initial_data = serde_json::to_vec(&initial).expect("initial usage snapshot should encode");
store.objects.lock().await.insert(key.clone(), initial_data.clone());
store.revisions.lock().await.insert(key.clone(), 1);
@@ -5054,6 +5079,7 @@ mod tests {
deleted.bucket_sizes.clear();
deleted.buckets_count = 0;
deleted.calculate_totals();
mark_usage_snapshot_complete(&mut deleted);
store
.interleaving_puts
.lock()
@@ -5102,7 +5128,7 @@ mod tests {
last_update: Some(std::time::SystemTime::UNIX_EPOCH + Duration::from_secs(20)),
scanner_epoch: Some(8),
scanner_cycle: Some(1),
..Default::default()
..complete_usage_with_bucket_count(None, 0)
};
store.objects.lock().await.insert(
backup_key.clone(),
@@ -5129,6 +5155,7 @@ mod tests {
incoming.bucket_sizes.insert("bucket-a".to_string(), 84);
incoming.buckets_count = 1;
incoming.calculate_totals();
mark_usage_snapshot_complete(&mut incoming);
sender.send(incoming).await.expect("usage snapshot should enqueue");
}
drop(sender);
@@ -5162,7 +5189,7 @@ mod tests {
last_update: Some(std::time::SystemTime::UNIX_EPOCH + Duration::from_secs(30)),
scanner_epoch: Some(8),
scanner_cycle: Some(10),
..Default::default()
..complete_usage_with_bucket_count(None, 0)
};
let encoded = serde_json::to_vec(&durable).expect("usage snapshot should encode");
store.objects.lock().await.insert(main_key.clone(), encoded.clone());
@@ -5205,6 +5232,7 @@ mod tests {
incoming.bucket_sizes.insert("bucket-a".to_string(), 84);
incoming.buckets_count = 1;
incoming.calculate_totals();
mark_usage_snapshot_complete(&mut incoming);
let mut deleted = incoming.clone();
deleted.last_update = Some(std::time::SystemTime::UNIX_EPOCH + Duration::from_secs(31));
@@ -5212,6 +5240,7 @@ mod tests {
deleted.bucket_sizes.clear();
deleted.buckets_count = 0;
deleted.calculate_totals();
mark_usage_snapshot_complete(&mut deleted);
store.replace_after_successful_puts.lock().await.insert(
main_key.clone(),
(1, serde_json::to_vec(&deleted).expect("deleted primary snapshot should encode")),
@@ -5251,21 +5280,9 @@ mod tests {
let (sender, receiver) = mpsc::channel(1);
let ctx = CancellationToken::new();
let key = memory_config_key(RUSTFS_META_BUCKET, DATA_USAGE_OBJ_NAME_PATH.as_str());
let initial = DataUsageInfo {
last_update: Some(std::time::SystemTime::UNIX_EPOCH + Duration::from_secs(10)),
buckets_count: 1,
..Default::default()
};
let stale_winner = DataUsageInfo {
last_update: Some(std::time::SystemTime::UNIX_EPOCH + Duration::from_secs(20)),
buckets_count: 2,
..Default::default()
};
let current = DataUsageInfo {
last_update: Some(std::time::SystemTime::UNIX_EPOCH + Duration::from_secs(30)),
buckets_count: 3,
..Default::default()
};
let initial = complete_usage_with_bucket_count(Some(std::time::SystemTime::UNIX_EPOCH + Duration::from_secs(10)), 3);
let stale_winner = complete_usage_with_bucket_count(Some(std::time::SystemTime::UNIX_EPOCH + Duration::from_secs(20)), 3);
let current = complete_usage_with_bucket_count(Some(std::time::SystemTime::UNIX_EPOCH + Duration::from_secs(30)), 3);
store
.objects
.lock()
@@ -5298,21 +5315,13 @@ mod tests {
}
#[tokio::test]
async fn test_store_data_usage_in_backend_rejects_untimestamped_stale_snapshot() {
async fn test_store_data_usage_in_backend_rejects_untimestamped_complete_snapshot() {
let store = Arc::new(MemoryConfigStore::default());
let (sender, receiver) = mpsc::channel(2);
let ctx = CancellationToken::new();
let timestamped = DataUsageInfo {
last_update: Some(std::time::SystemTime::UNIX_EPOCH + Duration::from_secs(20)),
buckets_count: 2,
..Default::default()
};
let untimestamped = DataUsageInfo {
last_update: None,
buckets_count: 1,
..Default::default()
};
let timestamped = complete_usage_with_bucket_count(Some(std::time::SystemTime::UNIX_EPOCH + Duration::from_secs(20)), 2);
let untimestamped = complete_usage_with_bucket_count(None, 1);
sender
.send(timestamped)
@@ -5334,7 +5343,7 @@ mod tests {
assert_eq!(saved.buckets_count, 2);
assert_eq!(saved.last_update, Some(std::time::SystemTime::UNIX_EPOCH + Duration::from_secs(20)));
assert_eq!(outcome, DataUsagePersistOutcome::Current);
assert_eq!(outcome, DataUsagePersistOutcome::Failed);
}
#[tokio::test]
@@ -5346,8 +5355,7 @@ mod tests {
let snapshot = DataUsageInfo {
last_update: Some(std::time::SystemTime::UNIX_EPOCH + Duration::from_secs(20)),
scanner_cycle: Some(12),
buckets_count: 2,
..Default::default()
..complete_usage_with_bucket_count(None, 2)
};
store
.objects
@@ -5377,8 +5385,7 @@ mod tests {
last_update: Some(std::time::SystemTime::UNIX_EPOCH + Duration::from_secs(20)),
scanner_epoch: Some(8),
scanner_cycle: Some(12),
buckets_count: 2,
..Default::default()
..complete_usage_with_bucket_count(None, 2)
};
store
.objects
@@ -5392,8 +5399,7 @@ mod tests {
last_update: Some(std::time::SystemTime::UNIX_EPOCH + Duration::from_secs(30)),
scanner_epoch: Some(8),
scanner_cycle: Some(12),
buckets_count: 3,
..Default::default()
..complete_usage_with_bucket_count(None, 3)
})
.await
.expect("changed retry snapshot should enqueue");
@@ -5426,8 +5432,7 @@ mod tests {
let existing = DataUsageInfo {
last_update: Some(std::time::SystemTime::UNIX_EPOCH + Duration::from_secs(200)),
scanner_cycle: Some(12),
buckets_count: 2,
..Default::default()
..complete_usage_with_bucket_count(None, 2)
};
store
.objects
@@ -5440,8 +5445,7 @@ mod tests {
let older = DataUsageInfo {
last_update: Some(std::time::SystemTime::UNIX_EPOCH + Duration::from_secs(300)),
scanner_cycle: Some(11),
buckets_count: 1,
..Default::default()
..complete_usage_with_bucket_count(None, 1)
};
older_sender.send(older).await.expect("older-cycle snapshot should enqueue");
drop(older_sender);
@@ -5454,8 +5458,7 @@ mod tests {
let newer = DataUsageInfo {
last_update: Some(std::time::SystemTime::UNIX_EPOCH + Duration::from_secs(100)),
scanner_cycle: Some(13),
buckets_count: 3,
..Default::default()
..complete_usage_with_bucket_count(None, 3)
};
newer_sender
.send(newer.clone())
@@ -5490,8 +5493,7 @@ mod tests {
last_update: Some(std::time::SystemTime::UNIX_EPOCH + Duration::from_secs(200)),
scanner_epoch: Some(8),
scanner_cycle: Some(12),
buckets_count: 2,
..Default::default()
..complete_usage_with_bucket_count(None, 2)
};
store
.objects
@@ -5506,8 +5508,7 @@ mod tests {
last_update: Some(std::time::SystemTime::UNIX_EPOCH + Duration::from_secs(300)),
scanner_epoch: Some(7),
scanner_cycle: Some(99),
buckets_count: 1,
..Default::default()
..complete_usage_with_bucket_count(None, 1)
})
.await
.expect("old-epoch snapshot should enqueue");
@@ -5523,8 +5524,7 @@ mod tests {
last_update: Some(std::time::SystemTime::UNIX_EPOCH + Duration::from_secs(100)),
scanner_epoch: None,
scanner_cycle: Some(1),
buckets_count: 3,
..Default::default()
..complete_usage_with_bucket_count(None, 3)
})
.await
.expect("replacement-epoch snapshot should enqueue");
@@ -5554,8 +5554,7 @@ mod tests {
let existing = DataUsageInfo {
last_update: Some(std::time::SystemTime::UNIX_EPOCH + Duration::from_secs(100)),
scanner_cycle: Some(12),
buckets_count: 2,
..Default::default()
..complete_usage_with_bucket_count(None, 2)
};
store
.objects
@@ -5569,8 +5568,7 @@ mod tests {
.send(DataUsageInfo {
last_update: Some(std::time::SystemTime::UNIX_EPOCH + Duration::from_secs(300)),
scanner_cycle: Some(12),
buckets_count: 3,
..Default::default()
..complete_usage_with_bucket_count(None, 3)
})
.await
.expect("conflicting same-cycle snapshot should enqueue");
@@ -5595,11 +5593,59 @@ mod tests {
);
}
fn usage_with_last_update(last_update: Option<std::time::SystemTime>) -> DataUsageInfo {
DataUsageInfo {
#[tokio::test]
async fn test_store_data_usage_in_backend_rejects_incomplete_snapshot() {
let store = Arc::new(MemoryConfigStore::default());
let (sender, receiver) = mpsc::channel(2);
let complete_update = std::time::SystemTime::UNIX_EPOCH + Duration::from_secs(10);
sender
.send(complete_usage_with_bucket_count(Some(complete_update), 1))
.await
.expect("complete usage snapshot should enqueue");
sender
.send(DataUsageInfo {
last_update: Some(complete_update + Duration::from_secs(1)),
buckets_count: 1,
..Default::default()
})
.await
.expect("incomplete usage snapshot should enqueue");
drop(sender);
let outcome = store_data_usage_in_backend_with_outcome(CancellationToken::new(), store.clone(), receiver).await;
let objects = store.objects.lock().await;
let saved = objects
.get(&memory_config_key(RUSTFS_META_BUCKET, DATA_USAGE_OBJ_NAME_PATH.as_str()))
.expect("complete data usage snapshot should remain saved");
let saved = serde_json::from_slice::<DataUsageInfo>(saved).expect("saved usage snapshot should decode");
assert_eq!(saved.last_update, Some(complete_update));
assert!(saved.is_complete_bucket_usage_snapshot());
assert_eq!(outcome, DataUsagePersistOutcome::Failed);
}
fn mark_usage_snapshot_complete(info: &mut DataUsageInfo) {
info.usage_snapshot_complete = true;
}
fn complete_usage_with_bucket_count(last_update: Option<std::time::SystemTime>, buckets_count: u64) -> DataUsageInfo {
let mut info = DataUsageInfo {
last_update,
buckets_count,
usage_snapshot_complete: true,
..Default::default()
};
for index in 0..buckets_count {
let bucket = format!("bucket-{index}");
info.buckets_usage.insert(bucket.clone(), Default::default());
info.bucket_sizes.insert(bucket, 0);
}
info
}
fn usage_with_last_update(last_update: Option<std::time::SystemTime>) -> DataUsageInfo {
complete_usage_with_bucket_count(last_update, 0)
}
#[test]
@@ -5681,11 +5727,10 @@ mod tests {
for idx in 1_u64..=11 {
sender
.send(DataUsageInfo {
last_update: Some(std::time::SystemTime::UNIX_EPOCH + Duration::from_secs(idx)),
buckets_count: idx,
..Default::default()
})
.send(complete_usage_with_bucket_count(
Some(std::time::SystemTime::UNIX_EPOCH + Duration::from_secs(idx)),
idx,
))
.await
.expect("usage snapshot should enqueue");
}
+47 -4
View File
@@ -846,7 +846,7 @@ fn apply_bucket_result_to_cache(cache: &mut DataUsageCache, result: DataUsageEnt
}
fn should_publish_completed_snapshot(completed_count: usize, total_count: usize, budget_elapsed: bool, cancelled: bool) -> bool {
total_count > 0 && completed_count == total_count && !budget_elapsed && !cancelled
completed_count == total_count && !budget_elapsed && !cancelled
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
@@ -1015,6 +1015,10 @@ fn completed_data_usage_info(
}
let merged_last_update = results.iter().filter_map(|result| result.info.last_update).max()?;
let bucket_sizes = buckets_usage
.iter()
.map(|(bucket, usage)| (bucket.clone(), usage.size))
.collect();
let data_usage_info = DataUsageInfo {
last_update: Some(merged_last_update),
scanner_cycle: Some(results.first()?.info.next_cycle),
@@ -1023,7 +1027,9 @@ fn completed_data_usage_info(
delete_markers_total_count: u64::try_from(total.delete_markers).ok()?,
objects_total_size: u64::try_from(total.size).ok()?,
buckets_count: u64::try_from(all_buckets.len()).ok()?,
bucket_sizes,
buckets_usage,
usage_snapshot_complete: true,
..Default::default()
};
Some((data_usage_info, merged_last_update))
@@ -1042,7 +1048,10 @@ mod publish_gate_tests {
assert!(!should_publish_completed_snapshot(2, 3, false, false));
assert!(!should_publish_completed_snapshot(3, 3, true, false));
assert!(!should_publish_completed_snapshot(3, 3, false, true));
assert!(!should_publish_completed_snapshot(0, 0, false, false));
assert!(
should_publish_completed_snapshot(0, 0, false, false),
"a completed empty namespace is an authoritative zero snapshot"
);
}
fn incomplete_scope_cache(source: DataUsageCacheSource) -> DataUsageCache {
@@ -1121,11 +1130,13 @@ mod publish_gate_tests {
#[test]
fn completed_data_usage_info_requires_every_set_before_publish() {
let all_buckets = vec!["bucket-a".to_string(), "bucket-b".to_string()];
let all_buckets = vec!["bucket-a".to_string(), "bucket-b".to_string(), "bucket-empty".to_string()];
let mut first_set = completed_root_cache("bucket-a", 1, 10, DataUsageCacheSource::new(0, 0));
first_set.replace("bucket-b", DATA_USAGE_ROOT, DataUsageEntry::default());
first_set.replace("bucket-empty", DATA_USAGE_ROOT, DataUsageEntry::default());
let mut second_set = completed_root_cache("bucket-b", 2, 20, DataUsageCacheSource::new(1, 0));
second_set.replace("bucket-a", DATA_USAGE_ROOT, DataUsageEntry::default());
second_set.replace("bucket-empty", DATA_USAGE_ROOT, DataUsageEntry::default());
assert!(
completed_data_usage_info_for_test(&[first_set.clone(), DataUsageCache::default()], &all_buckets, false, false)
@@ -1144,7 +1155,38 @@ mod publish_gate_tests {
assert_eq!(last_update, SystemTime::UNIX_EPOCH + Duration::from_secs(20));
assert_eq!(data_usage_info.scanner_cycle, Some(0));
assert_eq!(data_usage_info.objects_total_count, 3);
assert_eq!(data_usage_info.buckets_usage.len(), 2);
assert_eq!(data_usage_info.buckets_usage.len(), 3);
assert!(data_usage_info.usage_snapshot_complete);
assert_eq!(
data_usage_info
.buckets_usage
.get("bucket-empty")
.map(|usage| (usage.objects_count, usage.size)),
Some((0, 0))
);
}
#[test]
fn completed_data_usage_info_publishes_confirmed_empty_namespace() {
let mut completed_set = DataUsageCache {
info: DataUsageCacheInfo {
name: DATA_USAGE_ROOT.to_string(),
last_update: Some(SystemTime::UNIX_EPOCH + Duration::from_secs(10)),
source: Some(DataUsageCacheSource::new(0, 0)),
snapshot_complete: true,
scan_plan_digest: Some(TEST_PLAN_DIGEST),
..Default::default()
},
..Default::default()
};
completed_set.replace(DATA_USAGE_ROOT, "", DataUsageEntry::default());
let (data_usage_info, _) = completed_data_usage_info_for_test(&[completed_set], &[], false, false)
.expect("a completed empty namespace should produce an authoritative snapshot");
assert!(data_usage_info.is_complete_bucket_usage_snapshot());
assert_eq!(data_usage_info.buckets_count, 0);
assert!(data_usage_info.buckets_usage.is_empty());
}
#[test]
@@ -2000,6 +2042,7 @@ impl ScannerIOCycle for ECStore {
let empty_usage = DataUsageInfo {
last_update: Some(SystemTime::now()),
scanner_cycle: Some(want_cycle),
usage_snapshot_complete: true,
..Default::default()
};
send_data_usage_update(&updates, empty_usage).await?;
+10 -6
View File
@@ -57,7 +57,10 @@ pub(crate) use rustfs_ecstore::api::config::init as ecstore_config_init;
pub(crate) use rustfs_ecstore::api::config::storageclass::{
RRS as ECSTORE_STORAGECLASS_RRS, STANDARD as ECSTORE_STORAGECLASS_STANDARD,
};
pub(crate) use rustfs_ecstore::api::data_usage::replace_bucket_usage_memory_from_info as ecstore_replace_bucket_usage_memory_from_info;
pub(crate) use rustfs_ecstore::api::data_usage::{
invalidate_data_usage_snapshot_cache as ecstore_invalidate_data_usage_snapshot_cache,
replace_bucket_usage_memory_from_info as ecstore_replace_bucket_usage_memory_from_info,
};
#[cfg(test)]
pub(crate) use rustfs_ecstore::api::disk::endpoint::Endpoint as EcstoreEndpoint;
pub(crate) use rustfs_ecstore::api::disk::error::{DiskError as EcstoreDiskError, Result as EcstoreDiskResult};
@@ -107,11 +110,12 @@ pub(crate) mod owner {
EcstoreSetDisks, EcstoreStorageError, EcstoreStore, EcstoreTierConfig, EcstoreVersioningApi,
ScannerReplicationHealObject, ScannerReplicationHealResult, ScannerReplicationQueueAdmission, ecstore_apply_expiry_rule,
ecstore_apply_transition_rule, ecstore_expiry_state_handle, ecstore_get_global_tier_config_mgr,
ecstore_get_lifecycle_config, ecstore_get_object_lock_config, ecstore_get_replication_config, ecstore_is_erasure,
ecstore_is_erasure_sd, ecstore_is_reserved_or_invalid_bucket, ecstore_list_path_raw,
ecstore_object_opts_from_object_info, ecstore_path2_bucket_object, ecstore_path2_bucket_object_with_base_path,
ecstore_read_config, ecstore_replace_bucket_usage_memory_from_info, ecstore_resolve_object_store_handle,
ecstore_save_config, scanner_replication_config_for_lifecycle_eval,
ecstore_get_lifecycle_config, ecstore_get_object_lock_config, ecstore_get_replication_config,
ecstore_invalidate_data_usage_snapshot_cache, ecstore_is_erasure, ecstore_is_erasure_sd,
ecstore_is_reserved_or_invalid_bucket, ecstore_list_path_raw, ecstore_object_opts_from_object_info,
ecstore_path2_bucket_object, ecstore_path2_bucket_object_with_base_path, ecstore_read_config,
ecstore_replace_bucket_usage_memory_from_info, ecstore_resolve_object_store_handle, ecstore_save_config,
scanner_replication_config_for_lifecycle_eval,
};
#[cfg(test)]