Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 28 additions & 4 deletions Sources/Cacheout/Cleaner/CacheCleaner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ actor CacheCleaner {
if moveToTrash {
try await trashDirectory(url)
} else {
try removeContents(of: url)
try await removeContents(of: url, fileManager: fileManager)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep permanent-clean path actor-isolated

Changing permanent deletion from removeContents (sync, actor-isolated) to await removeContents with a nonisolated implementation introduces a suspension point in the clean loop, so CacheCleaner can now interleave another clean request while the first one is still deleting files. If two clean operations overlap (for example, duplicate UI triggers or concurrent callers), both runs can enumerate/delete the same paths and produce spurious ENOENT errors or inconsistent cleanup reports. The previous synchronous path did not permit this interleaving for permanent deletes.

Useful? React with πŸ‘Β / πŸ‘Ž.

}
categoryFreed += result.sizeBytes
} catch {
Expand Down Expand Up @@ -133,12 +133,36 @@ actor CacheCleaner {
}
}

private func removeContents(of url: URL) throws {
// ⚑ Bolt Optimization: Parallelize bulk I/O operations using a sliding window TaskGroup
// and Task.detached to prevent cooperative thread pool exhaustion.
nonisolated private func removeContents(of url: URL, fileManager: FileManager) async throws {
let contents = try fileManager.contentsOfDirectory(
at: url, includingPropertiesForKeys: nil
)
for item in contents {
try fileManager.removeItem(at: item)

try await withThrowingTaskGroup(of: Void.self) { group in
let maxConcurrency = 8
var iterator = contents.makeIterator()

for _ in 0..<maxConcurrency {
if let item = iterator.next() {
group.addTask {
try await Task.detached {
try fileManager.removeItem(at: item)
}.value
}
}
}

for try await _ in group {
if let item = iterator.next() {
group.addTask {
try await Task.detached {
try fileManager.removeItem(at: item)
}.value
}
}
}
}
}

Expand Down
Loading