fix(scanner): publish partial usage for compacted scans (#3277)

* fix(scanner): publish partial usage for compacted scans

* fix(scanner): publish first partial usage immediately

* fix(scanner): skip startup delay for cold usage cache

* fix(scanner): tighten cold usage publish gate

---------

Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
Co-authored-by: houseme <housemecn@gmail.com>
This commit is contained in:
Henry Guo
2026-06-08 14:18:35 +08:00
committed by GitHub
parent 4751a9f4b9
commit f40dd2f93c
3 changed files with 331 additions and 14 deletions
+62 -4
View File
@@ -1024,14 +1024,18 @@ impl FolderScanner {
/// Send update if enough time has passed
/// Should be called on a regular basis when the new_cache contains more recent total than previously.
/// May or may not send an update upstream.
pub async fn send_update(&mut self) {
// Send at most an update every minute.
fn should_send_update(&self) -> bool {
if self.updates.is_none() {
return;
return false;
}
let elapsed = self.last_update.elapsed().unwrap_or(Duration::from_secs(0));
if elapsed < Duration::from_secs(60) {
elapsed >= Duration::from_secs(60)
}
pub async fn send_update(&mut self) {
// Send at most an update every minute.
if !self.should_send_update() {
return;
}
@@ -1046,6 +1050,15 @@ impl FolderScanner {
}
}
async fn send_update_for_entry(&mut self, hash: &DataUsageHash, parent: &Option<DataUsageHash>, entry: &DataUsageEntry) {
if !self.should_send_update() {
return;
}
self.update_cache.replace_hashed(hash, parent, entry);
self.send_update().await;
}
/// Scan a folder recursively
/// Files found in the folders will be added to new_cache.
#[allow(clippy::never_loop)]
@@ -1300,6 +1313,7 @@ impl FolderScanner {
}
if should_yield_after_object(object_count, yield_every_objects) {
self.send_update_for_entry(&this_hash, &folder.parent, into).await;
let yield_start = Instant::now();
tokio::task::yield_now().await;
global_metrics().record_scanner_yield(yield_start.elapsed());
@@ -1445,6 +1459,7 @@ impl FolderScanner {
let fut = Box::pin(self.scan_folder(ctx.clone(), folder_item.clone(), into));
fut.await.map_err(|e| ScannerError::Other(e.to_string()))?;
self.record_scan_resume_hint(&folder_item.name);
self.send_update_for_entry(&this_hash, &folder.parent, into).await;
tokio::task::yield_now().await;
} else {
let mut dst = DataUsageEntry::default();
@@ -1694,6 +1709,7 @@ impl FolderScanner {
// In compacted mode child totals are accumulated directly into the parent entry.
let fut = Box::pin(self.scan_folder(ctx.clone(), folder_item.clone(), into));
fut.await.map_err(|e| ScannerError::Other(e.to_string()))?;
self.send_update_for_entry(&this_hash, &folder.parent, into).await;
tokio::task::yield_now().await;
} else {
let mut dst = DataUsageEntry::default();
@@ -2601,6 +2617,48 @@ mod tests {
assert!(budget.token().is_cancelled());
}
#[tokio::test]
#[serial]
async fn test_scan_folder_compacted_parent_sends_partial_update() {
let (mut scanner, temp_dir) = build_test_scanner().await;
let _guard = TestGuard::new(60, 100, &mut scanner, temp_dir.clone());
let bucket_dir = temp_dir.join("bucket");
tokio::fs::create_dir_all(bucket_dir.join("child"))
.await
.expect("failed to create child directory");
scanner.old_cache.info.name = "bucket".to_string();
scanner.new_cache.info.name = "bucket".to_string();
scanner.update_cache.info.name = "bucket".to_string();
scanner.last_update = SystemTime::UNIX_EPOCH;
let (tx, mut rx) = mpsc::channel(1);
scanner.updates = Some(tx);
let folder = CachedFolder {
name: "bucket".to_string(),
parent: None,
object_heal_prob_div: 1,
};
let mut into = DataUsageEntry {
compacted: true,
..Default::default()
};
scanner
.scan_folder(CancellationToken::new(), folder, &mut into)
.await
.expect("compacted scan should finish successfully");
let update = tokio::time::timeout(Duration::from_secs(1), rx.recv())
.await
.expect("compacted scan should send a partial update")
.expect("partial update channel should remain open");
assert!(update.compacted, "partial update should preserve compacted state");
}
#[tokio::test]
#[serial]
async fn test_scan_data_folder_returns_partial_cache_on_budget_cancel() {