mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-27 15:37:02 +00:00
fix(object-capacity): make dirty-disk lifecycle race-free and prune ghost entries (#4550)
Two dirty-mark lifecycle gaps (S19+S30): - A commit cleared every dirty mark for the disks it scanned, even marks recorded while the walker was already past the written prefix, so the next dirty-subset refresh served stale cached bytes as exact until the next full scan. Dirty marks now carry the instant they were last recorded and a commit only clears marks that predate its scan start (CapacityUpdate::scan_started_at, set by refresh_capacity_with_scope). - The write side marks every disk of an EC set dirty, including remote peers, while scans and clearing only cover local disks — remote or removed disks stayed marked forever, keeping the dirty-disk gauge permanently non-zero with bounded memory stuck behind it. The refresh disk selection now prunes dirty entries outside the current local topology before consulting them. Ref: rustfs/backlog#1020 (S19+S30 from audit rustfs/backlog#1010)
This commit is contained in:
@@ -312,6 +312,12 @@ pub async fn select_capacity_refresh_disks(
|
||||
capacity_manager: &HybridCapacityManager,
|
||||
disks: &[CapacityDiskRef],
|
||||
) -> (Vec<CapacityDiskRef>, bool) {
|
||||
// The write side marks every disk of an EC set dirty, including remote
|
||||
// peers, but only local disks are ever scanned and cleared — drop ghost
|
||||
// entries so the dirty gauge reflects local pending work (backlog#1020).
|
||||
let local_set: HashSet<CapacityScopeDisk> = disks.iter().map(disk_scope_key).collect();
|
||||
capacity_manager.retain_dirty_disks_within(&local_set).await;
|
||||
|
||||
if !capacity_manager.can_refresh_dirty_subset().await {
|
||||
return (disks.to_vec(), false);
|
||||
}
|
||||
@@ -336,6 +342,7 @@ pub async fn select_capacity_refresh_disks(
|
||||
}
|
||||
|
||||
pub async fn refresh_capacity_with_scope(disks: Vec<CapacityDiskRef>, dirty_subset: bool) -> Result<CapacityUpdate, String> {
|
||||
let scan_started_at = Instant::now();
|
||||
let report = calculate_data_dir_used_capacity_report(&disks)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
@@ -344,7 +351,9 @@ pub async fn refresh_capacity_with_scope(disks: Vec<CapacityDiskRef>, dirty_subs
|
||||
return Err("dirty subset refresh had partial errors".to_string());
|
||||
}
|
||||
|
||||
Ok(report.into_capacity_update(disks.len(), !dirty_subset))
|
||||
let mut update = report.into_capacity_update(disks.len(), !dirty_subset);
|
||||
update.scan_started_at = Some(scan_started_at);
|
||||
Ok(update)
|
||||
}
|
||||
|
||||
/// Scan the provided local disk roots and return a summarized used-capacity result.
|
||||
@@ -1564,6 +1573,7 @@ mod tests {
|
||||
expected_disk_count: Some(2),
|
||||
replaces_disk_cache: true,
|
||||
clear_dirty_disks: Vec::new(),
|
||||
scan_started_at: None,
|
||||
},
|
||||
DataSource::RealTime,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user