refactor(scanner): split data_usage_define persistence and tests (#6292)

Split the 3655-line data_usage_define.rs (59% inline tests) into a
canonical foo.rs + foo/ module tree with zero behavior change:

- data_usage_define.rs (~950): cache constants and revision helpers,
  the data-usage tree types, DataUsageCacheInfo with its hand-written
  Serialize, the in-memory tree operations, dui, and marshal/unmarshal
- data_usage_define/persistence.rs (~580): the load/backup/restore
  ladder (load, try_load_inner, revision_for_path) and the CAS save
  path with its retry policy and save metrics
- data_usage_define/tests.rs (~2155): the inline test module as a child
  module

All module paths are unchanged (the lib.rs data_usage_define::* glob
re-export and every crate::data_usage_define:: consumer resolve as
before). The hand-written map-encoded Serialize for
DataUsageCacheInfo is moved byte-for-byte per the AGENTS.md
cross-cutting invariant; on-disk names and the cache key format const
stay in the root. Four persistence helpers used by tests gain
pub(super), whose scope equals the old single-module privacy domain.
Code is moved verbatim apart from those markers, per-module import
headers, and rustfmt re-wraps.

Co-authored-by: heihutu <heihutu@gmail.com>
This commit is contained in:
houseme
2026-08-20 12:23:09 +08:00
committed by GitHub
parent 769e66511f
commit c428c6615e
3 changed files with 2740 additions and 2707 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,581 @@
// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use super::*;
use std::sync::Arc;
pub(super) enum DataUsageCacheLoadAttempt {
Loaded {
cache: Box<DataUsageCache>,
revision: Option<DataUsageCacheRevision>,
},
Missing {
revision: Option<DataUsageCacheRevision>,
},
Corrupt {
revision: Option<DataUsageCacheRevision>,
},
Retryable(Error),
}
struct DataUsageCacheLoadResult {
cache: DataUsageCache,
main_revision: DataUsageCacheRevision,
backup_revision: Option<DataUsageCacheRevision>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum DataUsageCacheLoadState {
Loaded,
Missing,
Corrupt,
Retryable,
}
impl DataUsageCacheLoadAttempt {
fn revision(&self) -> Option<DataUsageCacheRevision> {
match self {
Self::Loaded { revision, .. } | Self::Missing { revision } | Self::Corrupt { revision } => revision.clone(),
Self::Retryable(_) => None,
}
}
}
// Data usage paths (computed at runtime)
impl DataUsageCache {
/// Only backend errors are returned as errors.
/// The loader is optimistic and has no locking, but tries 5 times before giving up.
/// If the object is not found, a nil error with empty data usage cache is returned.
pub async fn load<S: ScannerObjectIO>(&mut self, store: Arc<S>, name: &str) -> StorageResult<()> {
let loaded = Self::load_cache(store, name).await?;
*self = loaded.cache;
Ok(())
}
pub(crate) async fn load_with_revisions<S: ScannerObjectIO>(
&mut self,
store: Arc<S>,
name: &str,
) -> StorageResult<DataUsageCacheRevisions> {
let backup_name = format!("{name}.bkp");
let backup_path = path_join_buf(&[BUCKET_META_PREFIX, &backup_name]);
let loaded = Self::load_cache(store.clone(), name).await?;
let backup = match loaded.backup_revision {
Some(revision) => Some(revision),
None => match Self::revision_for_path(store, &backup_path).await {
Ok(revision) => Some(revision),
Err(err) => {
counter!(METRIC_CACHE_BACKUP_REVISION_FAILURE_TOTAL).increment(1);
debug!(
target: "rustfs::scanner::data_usage",
event = EVENT_SCANNER_CACHE_LOAD_STATE,
component = LOG_COMPONENT_SCANNER,
subsystem = LOG_SUBSYSTEM_CACHE,
cache_name = %name,
backup_path = %backup_path,
state = "backup_revision_unavailable",
error = %err,
"Scanner cache backup revision lookup failed"
);
None
}
},
};
let main = loaded.main_revision;
*self = loaded.cache;
Ok(DataUsageCacheRevisions { main, backup })
}
async fn load_cache<S: ScannerObjectIO>(store: Arc<S>, name: &str) -> StorageResult<DataUsageCacheLoadResult> {
let mut last_retryable = None;
for attempt in 0..5 {
let main_attempt = Self::try_load_inner(store.clone(), name, Duration::from_secs(60)).await?;
let main_revision = main_attempt.revision();
let main_state = match main_attempt {
DataUsageCacheLoadAttempt::Loaded {
cache,
revision: Some(main_revision),
} => {
return Ok(DataUsageCacheLoadResult {
cache: *cache,
main_revision,
backup_revision: None,
});
}
DataUsageCacheLoadAttempt::Loaded { revision: None, .. } => {
last_retryable = Some(Error::other(format!("scanner cache object has no revision: {name}")));
DataUsageCacheLoadState::Retryable
}
DataUsageCacheLoadAttempt::Missing { .. } => DataUsageCacheLoadState::Missing,
DataUsageCacheLoadAttempt::Corrupt { .. } => DataUsageCacheLoadState::Corrupt,
DataUsageCacheLoadAttempt::Retryable(err) => {
last_retryable = Some(err);
DataUsageCacheLoadState::Retryable
}
};
if main_state == DataUsageCacheLoadState::Retryable {
if attempt < 4 {
let sleep_ms: u64 = rand::random::<u64>() % 1000;
sleep(Duration::from_millis(sleep_ms)).await;
}
continue;
}
let backup_name = format!("{name}.bkp");
let backup_attempt = Self::try_load_inner(store.clone(), &backup_name, Duration::from_secs(30)).await?;
let backup_revision = backup_attempt.revision();
let backup_state = match backup_attempt {
DataUsageCacheLoadAttempt::Loaded {
cache,
revision: Some(backup_revision),
} => {
if matches!(main_state, DataUsageCacheLoadState::Missing | DataUsageCacheLoadState::Corrupt) {
let main_revision = main_revision.ok_or_else(|| {
Error::other(format!("scanner cache main revision is unavailable while loading backup: {name}"))
})?;
return Ok(DataUsageCacheLoadResult {
cache: *cache,
main_revision,
backup_revision: Some(backup_revision),
});
}
DataUsageCacheLoadState::Loaded
}
DataUsageCacheLoadAttempt::Loaded { revision: None, .. } => {
last_retryable = Some(Error::other(format!("scanner cache backup object has no revision: {backup_name}")));
DataUsageCacheLoadState::Retryable
}
DataUsageCacheLoadAttempt::Missing { .. } => DataUsageCacheLoadState::Missing,
DataUsageCacheLoadAttempt::Corrupt { .. } => DataUsageCacheLoadState::Corrupt,
DataUsageCacheLoadAttempt::Retryable(err) => {
last_retryable = Some(err);
DataUsageCacheLoadState::Retryable
}
};
match (main_state, backup_state) {
(DataUsageCacheLoadState::Missing, DataUsageCacheLoadState::Missing) => {
return Ok(DataUsageCacheLoadResult {
cache: DataUsageCache::default(),
main_revision: main_revision
.ok_or_else(|| Error::other(format!("scanner cache missing state has no revision: {name}")))?,
backup_revision,
});
}
(DataUsageCacheLoadState::Corrupt, DataUsageCacheLoadState::Missing)
| (DataUsageCacheLoadState::Missing, DataUsageCacheLoadState::Corrupt)
| (DataUsageCacheLoadState::Corrupt, DataUsageCacheLoadState::Corrupt) => {
warn!(
target: "rustfs::scanner::data_usage",
event = EVENT_SCANNER_CACHE_LOAD_STATE,
component = LOG_COMPONENT_SCANNER,
subsystem = LOG_SUBSYSTEM_CACHE,
cache_name = %name,
state = "corrupt_cache_rebuild",
"Scanner cache is corrupt and will be rebuilt"
);
return Ok(DataUsageCacheLoadResult {
cache: DataUsageCache::default(),
main_revision: main_revision
.ok_or_else(|| Error::other(format!("scanner cache corrupt state has no revision: {name}")))?,
backup_revision,
});
}
_ => {}
}
if attempt < 4 {
let sleep_ms: u64 = rand::random::<u64>() % 1000;
sleep(Duration::from_millis(sleep_ms)).await;
}
}
warn!(
target: "rustfs::scanner::data_usage",
event = EVENT_SCANNER_CACHE_LOAD_STATE,
component = LOG_COMPONENT_SCANNER,
subsystem = LOG_SUBSYSTEM_CACHE,
cache_name = %name,
retries = 5,
state = "max_retries_reached",
"Scanner cache load reached retry limit"
);
Err(last_retryable.unwrap_or_else(|| Error::other(format!("scanner cache could not be loaded: {name}"))))
}
pub(super) async fn try_load_inner<S: ScannerObjectIO>(
store: Arc<S>,
load_name: &str,
timeout_duration: Duration,
) -> StorageResult<DataUsageCacheLoadAttempt> {
// Abandon if more than time.Minute, so we don't hold up scanner.
// drive timeout by default is 2 minutes, we do not need to wait longer.
let load_fut = async {
// First try: RUSTFS_META_BUCKET + BUCKET_META_PREFIX/name
let path = path_join_buf(&[BUCKET_META_PREFIX, load_name]);
match store
.get_object_reader(
RUSTFS_META_BUCKET,
&path,
None,
HeaderMap::new(),
&ObjectOptions {
no_lock: true,
..Default::default()
},
)
.await
{
Ok(mut reader) => {
let revision = reader
.object_info
.etag
.as_ref()
.filter(|etag| !etag.is_empty())
.cloned()
.map(DataUsageCacheRevision::Etag);
match reader.read_all().await {
Ok(data) => match DataUsageCache::unmarshal(&data) {
Ok(cache) => Ok((Some(cache), revision, false)),
Err(_) => Ok((None, revision, true)),
},
Err(e) => {
// Read error
Err(e)
}
}
}
Err(err) => {
match err {
Error::FileNotFound | Error::VolumeNotFound | Error::ObjectNotFound(_, _) | Error::BucketNotFound(_) => {
// Try second location: DATA_USAGE_BUCKET/name
match store
.get_object_reader(
&DATA_USAGE_BUCKET,
load_name,
None,
HeaderMap::new(),
&ObjectOptions {
no_lock: true,
..Default::default()
},
)
.await
{
Ok(mut reader) => match reader.read_all().await {
Ok(data) => match DataUsageCache::unmarshal(&data) {
Ok(cache) => Ok((Some(cache), Some(DataUsageCacheRevision::Missing), false)),
Err(_) => Ok((None, Some(DataUsageCacheRevision::Missing), true)),
},
Err(e) => Err(e),
},
Err(inner_err) => match inner_err {
Error::FileNotFound
| Error::VolumeNotFound
| Error::ObjectNotFound(_, _)
| Error::BucketNotFound(_) => {
// Object not found in both locations
Ok((None, Some(DataUsageCacheRevision::Missing), false))
}
Error::ErasureReadQuorum => {
// InsufficientReadQuorum - retry
Err(Error::ErasureReadQuorum)
}
_ => {
// Other storage errors - retry
if matches!(
inner_err,
Error::FaultyDisk | Error::DiskFull | Error::StorageFull | Error::SlowDown
) {
return Err(inner_err);
}
Err(inner_err)
}
},
}
}
Error::ErasureReadQuorum => {
// InsufficientReadQuorum - retry
Err(Error::ErasureReadQuorum)
}
_ => {
// Other storage errors - retry
if matches!(err, Error::FaultyDisk | Error::DiskFull | Error::StorageFull | Error::SlowDown) {
return Err(err);
}
Err(err)
}
}
}
}
};
match timeout(timeout_duration, load_fut).await {
Ok(Ok((Some(cache), revision, _))) => Ok(DataUsageCacheLoadAttempt::Loaded {
cache: Box::new(cache),
revision,
}),
Ok(Ok((None, revision, true))) => Ok(DataUsageCacheLoadAttempt::Corrupt { revision }),
Ok(Ok((None, revision, false))) => Ok(DataUsageCacheLoadAttempt::Missing { revision }),
Ok(Err(err)) => Ok(DataUsageCacheLoadAttempt::Retryable(err)),
Err(_) => Ok(DataUsageCacheLoadAttempt::Retryable(Error::other("scanner cache load timed out"))),
}
}
async fn revision_for_path<S: ScannerObjectIO>(store: Arc<S>, path: &str) -> StorageResult<DataUsageCacheRevision> {
match store
.get_object_reader(
RUSTFS_META_BUCKET,
path,
None,
HeaderMap::new(),
&ObjectOptions {
no_lock: true,
..Default::default()
},
)
.await
{
Ok(reader) => reader
.object_info
.etag
.filter(|etag| !etag.is_empty())
.map(DataUsageCacheRevision::Etag)
.ok_or_else(|| StorageError::other(format!("scanner cache object {path} has no ETag"))),
Err(Error::FileNotFound | Error::VolumeNotFound | Error::ObjectNotFound(_, _) | Error::BucketNotFound(_)) => {
Ok(DataUsageCacheRevision::Missing)
}
Err(err) => Err(err),
}
}
pub(super) fn cache_save_timeout() -> Duration {
crate::runtime_config::scanner_cache_save_timeout()
}
pub(crate) fn persistence_timeout() -> Duration {
Self::cache_save_timeout()
.saturating_mul(DATA_USAGE_CACHE_SAVE_RETRIES + 1)
.saturating_add(DATA_USAGE_CACHE_SAVE_RETRY_BACKOFF_MAX)
.saturating_add(Self::backup_cache_save_timeout(Self::cache_save_timeout()))
.saturating_add(DATA_USAGE_CACHE_PERSISTENCE_MARGIN)
}
fn backup_cache_save_timeout(timeout_duration: Duration) -> Duration {
timeout_duration.min(Duration::from_secs(DATA_USAGE_CACHE_BACKUP_SAVE_TIMEOUT_SECS_MAX))
}
fn record_save_attempt(path_type: &'static str, result: &'static str, duration: Duration) {
histogram!(METRIC_CACHE_SAVE_DURATION_SECONDS, "cache" => path_type).record(duration.as_secs_f64());
counter!(
METRIC_CACHE_SAVE_ATTEMPT_TOTAL,
"cache" => path_type,
"result" => result
)
.increment(1);
if result == "timeout" {
counter!(METRIC_CACHE_SAVE_TIMEOUT_TOTAL, "cache" => path_type).increment(1);
}
}
fn should_retry_save_error(err: &StorageError) -> bool {
// Usage-cache files are best-effort scanner checkpoints. Retrying namespace
// lock failures immediately only adds more lock traffic to the same hot object.
!matches!(
err,
StorageError::Lock(_)
| StorageError::NamespaceLockQuorumUnavailable { .. }
| StorageError::PreconditionFailed
| StorageError::ObjectNotFound(_, _)
)
}
pub(super) async fn retry_save_op<F, Fut>(
path_type: &'static str,
timeout_duration: Duration,
max_retries: u32,
mut save_op: F,
) -> StorageResult<()>
where
F: FnMut() -> Fut,
Fut: Future<Output = StorageResult<()>>,
{
let mut last_err: Option<StorageError> = None;
for attempt in 0..=max_retries {
let attempt_start = Instant::now();
let timeout_res = timeout(timeout_duration, save_op()).await;
let duration = attempt_start.elapsed();
match timeout_res {
Ok(Ok(())) => {
Self::record_save_attempt(path_type, "success", duration);
return Ok(());
}
Err(e) => {
Self::record_save_attempt(path_type, "timeout", duration);
last_err = Some(StorageError::other(format!("{e} after {timeout_duration:?}")));
}
Ok(Err(e)) => {
let should_retry = Self::should_retry_save_error(&e);
Self::record_save_attempt(path_type, if should_retry { "error" } else { "lock_error" }, duration);
last_err = Some(e);
if !should_retry {
break;
}
}
}
if last_err.is_some() && attempt < max_retries {
counter!(METRIC_CACHE_SAVE_RETRY_TOTAL, "cache" => path_type).increment(1);
let backoff_ms = 50_u64 * (1_u64 << attempt) + (rand::random::<u64>() % 100);
sleep(Duration::from_millis(backoff_ms)).await;
}
}
Err(last_err.unwrap_or_else(|| StorageError::other("Failed to save data usage cache".to_string())))
}
async fn save_path_with_retry<S: ScannerObjectIO>(
store: Arc<S>,
path: &str,
buf: &[u8],
timeout_duration: Duration,
max_retries: u32,
revision: Option<DataUsageCacheRevision>,
) -> StorageResult<()> {
Self::ensure_cache_save_metrics_registered();
let path_type = Self::cache_path_type(path);
let path = path.to_string();
let save_result = Self::retry_save_op(path_type, timeout_duration, max_retries, || {
let store_clone = store.clone();
let path_clone = path.clone();
let buf_clone = buf.to_vec();
let revision = revision.clone();
async move {
if let Some(revision) = revision {
save_config_with_preconditions(store_clone, &path_clone, buf_clone, revision.preconditions()).await?;
} else {
save_config(store_clone, &path_clone, buf_clone).await?;
}
Ok::<(), StorageError>(())
}
})
.await;
let Err(save_err) = save_result else {
return Ok(());
};
for attempt in 0..=max_retries {
let reconcile = timeout(timeout_duration, async {
let mut reader = store
.get_object_reader(
RUSTFS_META_BUCKET,
&path,
None,
HeaderMap::new(),
&ObjectOptions {
no_lock: true,
..Default::default()
},
)
.await?;
Ok::<bool, StorageError>(reader.read_all().await? == buf)
})
.await;
if matches!(reconcile, Ok(Ok(true))) {
Self::record_save_attempt(path_type, "reconciled", Duration::ZERO);
return Ok(());
}
if matches!(reconcile, Ok(Ok(false))) {
break;
}
if attempt < max_retries {
sleep(Duration::from_millis(50_u64 * (u64::from(attempt) + 1))).await;
}
}
Err(save_err)
}
pub async fn save<S: ScannerObjectIO>(&self, store: Arc<S>, name: &str) -> StorageResult<()> {
self.save_inner(store, name, None).await
}
pub(crate) async fn save_with_revisions<S: ScannerObjectIO>(
&self,
store: Arc<S>,
name: &str,
revisions: &DataUsageCacheRevisions,
) -> StorageResult<()> {
self.save_inner(store, name, Some(revisions)).await
}
async fn save_inner<S: ScannerObjectIO>(
&self,
store: Arc<S>,
name: &str,
revisions: Option<&DataUsageCacheRevisions>,
) -> StorageResult<()> {
let mut buf = Vec::new();
self.serialize(&mut rmp_serde::Serializer::new(&mut buf))?;
let timeout_duration = Self::cache_save_timeout();
let path = path_join_buf(&[BUCKET_META_PREFIX, name]);
Self::save_path_with_retry(
store.clone(),
&path,
&buf,
timeout_duration,
DATA_USAGE_CACHE_SAVE_RETRIES,
revisions.map(|revisions| revisions.main.clone()),
)
.await?;
let backup_name = format!("{name}.bkp");
let backup_path = path_join_buf(&[BUCKET_META_PREFIX, &backup_name]);
let backup_timeout_duration = Self::backup_cache_save_timeout(timeout_duration);
let backup_revision = revisions.and_then(|revisions| revisions.backup.clone());
if revisions.is_some() && backup_revision.is_none() {
return Ok(());
}
if let Err(e) = Self::save_path_with_retry(
store,
&backup_path,
&buf,
backup_timeout_duration,
DATA_USAGE_CACHE_BACKUP_SAVE_RETRIES,
backup_revision,
)
.await
{
warn!(
target: "rustfs::scanner::data_usage",
event = EVENT_SCANNER_CACHE_SAVE_STATE,
component = LOG_COMPONENT_SCANNER,
subsystem = LOG_SUBSYSTEM_CACHE,
cache_name = %name,
backup_path = %backup_path,
state = "backup_save_failed",
error = %e,
"Scanner cache backup save failed"
);
}
Ok(())
}
}
File diff suppressed because it is too large Load Diff