Skip to content

Conversation

@levb
Copy link
Contributor

@levb levb commented Dec 4, 2025

Results from a run on my dev environment from a snapshot-restored identical filesystem:
Old version:

  • "files": 23962,
  • "bytes": 100003299328,
  • "files_per_second": 102.68987202964917,
  • "bytes_per_second": 428567148.42396384,

New version with (randomly-picked) concurrency "max_concurrent_stat": 32, "max_concurrent_scan": 16, "max_concurrent_delete": 4:

  • "del_files": 24300,
  • "empty_dirs": 6,
  • "bytes": 101403889664,
  • "files_per_second": 443.1544835696119,
  • "bytes_per_second": 1849283471.4403222,

Logger: log,
mu: sync.RWMutex{},
basePath: filepath.Dir(opts.Path),
cacheRoot: Dir{
Copy link

Choose a reason for hiding this comment

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

Memory unbounded cache: The PR description mentions "not memory-bounded yet" for the global caching. The cacheRoot Dir struct recursively stores all directories and files in memory. For large filesystems with millions of files, this could consume excessive memory and potentially cause OOM. Consider implementing a memory limit or LRU eviction policy.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Understood, will be discussed in the review.


// reinsert the "younger" candidates back into the directory tree
c.timeit(ctx, "reinserting candidates", func() {
c.reinsertCandidates(batch[c.DeleteN:])
Copy link

Choose a reason for hiding this comment

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

Incomplete bounds check: When n reaches BatchN and the batch is processed, the code slices batch[c.DeleteN:] and batch[:c.DeleteN] without verifying that c.DeleteN <= len(batch). If c.DeleteN > c.BatchN due to misconfiguration, this will panic. Add validation in validateOptions() to ensure DeleteN <= BatchN.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There is already

	if c.BatchN < c.DeleteN {
		return errors.New("files-per-loop must be >= deletions-per-loop")
	}

@levb levb marked this pull request as ready for review December 5, 2025 19:53
@levb levb requested a review from djeebus December 5, 2025 19:55
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines 46 to 50
}
d.state = scanning
d.mu.Unlock()

absPath := c.abs(path, "")

Choose a reason for hiding this comment

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

P1 Badge Directory state not cleared on scan errors

When scanDir sets d.state = scanning, any subsequent failure while opening or reading the directory (e.g., os.Open errors at line 52 or stat errors later in the function) returns without resetting the state. Later attempts to scan the same directory will always see ErrBusy, so that subtree is never reconsidered and the cleaner can stall or stop short of the target when a single stat/open error happens.

Useful? React with 👍 / 👎.

Comment on lines +96 to +100
// file needs to be unlinked before it's returned
f := d.Files[len(d.Files)-1]
d.Files = d.Files[:len(d.Files)-1]

return &f, nil, nil

Choose a reason for hiding this comment

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

P1 Badge Newest files picked for deletion instead of oldest

Directories are sorted by access time ascending (d.sort), but randomSubdirOrOldestFile removes the last element from d.Files and returns it. Because the last element is the most recently accessed file after the sort, the cleaner will preferentially delete the newest files in each directory, leaving the oldest files untouched until all newer ones are gone, which inverts the intended eviction order.

Useful? React with 👍 / 👎.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The comment was not exactly true, but I added tests and refactored to make it cleaner

SeenFileC atomic.Uint64

DeletedBytes atomic.Uint64
DeletedAges []time.Duration
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@djeebus We are not doing much with this, we could just maintain the running average without expensing the memory. Or, implement a log-scale histogram to keep track of the deleted ages?

(ditto for btime, not currently using it at all)

Comment on lines +96 to +100
// file needs to be unlinked before it's returned
f := d.Files[len(d.Files)-1]
d.Files = d.Files[:len(d.Files)-1]

return &f, nil, nil
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The comment was not exactly true, but I added tests and refactored to make it cleaner

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants