Skip to content

fix(cache): one cleanup thread per cache directory, not per manager - #453

Merged
ChuckBuilds merged 1 commit into
mainfrom
fix/one-cleanup-thread-per-cache-dir
Aug 12, 2026
Merged

fix(cache): one cleanup thread per cache directory, not per manager#453
ChuckBuilds merged 1 commit into
mainfrom
fix/one-cleanup-thread-per-cache-dir

Conversation

@ChuckBuilds

Copy link
Copy Markdown
Owner

What

The display process ran three cleanup threads over one cache directory:

14:22:59.954  display_controller         (the real manager)
14:22:59.973  startup validation, run 1  (discarded)
14:23:01.055  startup validation, run 2  (discarded)

Two of those CacheManagers existed only to read a directory path. StartupValidator._validate_cache_directory built an entire one to call get_cache_dir(), and validation runs twice — display_controller.py:93 before the plugin manager exists, and :261 again after. Each construction also probes writability by writing and deleting .writetest on the card.

Why they never went away

cleanup_loop is a closure over self, so the thread keeps its manager alive. Those two discarded objects could never be garbage collected, and woke every 24 hours to re-scan the same 9,000-file directory. Nothing stopped them either — stop_cleanup_thread had zero callers anywhere in the tree.

The fix

1. The validator uses the cache manager it's given. Also the more correct thing to validate: it was reporting on a cache the application doesn't use. When no caller supplies one it still builds its own — but stops the thread afterwards instead of abandoning it.

2. CacheManager tracks which directory it's sweeping. A second manager over the same directory skips starting a thread. That's the right granularity independent of call sites: the sweep lists a directory and deletes from it, so a second thread only duplicates the scan. Ownership is released on stop, so a survivor can take over rather than leaving the directory permanently unclaimed by a dead owner.

Measured

three managers, one directory:   OLD: 3 threads    NEW: 1 thread

Tests

9 new tests in test/test_cache_cleanup_thread_ownership.py: one manager starts one thread, three still start one, the first owns it, a survivor can take over, stopping a non-owner doesn't unclaim the directory, separate directories still get separate threads, twelve constructions leak nothing, and both validator paths (given a manager / building its own) leave no thread behind.

2775 passed, 60 skipped across the suite. The one failure, test_install_lowmem.py::TestDiskBackedTmpdir, is pre-existing on main and environment-dependent — this rig's /tmp is already disk-backed.

Scope

Deliberately not a singleton. CacheManager.__init__ takes no arguments, so a process-wide instance would be natural and would also collapse the duplicated memory tier and _write_digests map — but it touches every call site and changes test isolation, so it belongs in its own PR if wanted.

Also left alone: background_data_service.get_sport_cache_key() builds a full manager to compute f"{sport}_{date_str}", which uses no instance state. I checked the callers — all four sports plugins pass cache_key explicitly, so that branch never runs in production. A footgun worth a one-line fix, but not a live leak, and not this PR.

Honest scope note

This is a thread leak and a startup-tidiness fix, not an SD-wear or framerate win like #269 and #452. Two leaked threads and two redundant .writetest writes per boot.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Udr6MfaFLUPhX5Fgo67Jf5

The display process ran three cleanup threads over one directory:

    14:22:59.954  display_controller        (the real manager)
    14:22:59.973  startup validation, run 1 (discarded)
    14:23:01.055  startup validation, run 2 (discarded)

Two of those managers existed only to read a directory path.
StartupValidator._validate_cache_directory built a whole CacheManager to
call get_cache_dir(), and validation runs twice -- once before the
plugin manager exists and again after. Each construction also probes
writability by writing and deleting .writetest on the card.

The discarded ones never went away. cleanup_loop closes over `self`, so
the thread keeps its manager alive: two objects that could never be
collected, waking every 24 hours to re-scan the same 9,000-file
directory. Nothing stopped them either -- stop_cleanup_thread had no
callers anywhere in the tree.

Two changes. The validator now takes the CacheManager the application
actually uses, which is also the more correct thing to validate; when
no caller supplies one it still builds its own, but stops the thread
afterwards. And CacheManager now tracks which directory it is sweeping,
so the second manager over a directory skips starting a thread at all.
That is the right granularity regardless of call sites: the sweep lists
a directory and deletes from it, so a second thread only duplicates the
scan. Ownership is released on stop, so a survivor can take over rather
than leaving the directory permanently unclaimed by a dead owner.

Measured directly, three managers over one directory: 3 threads before,
1 after.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Udr6MfaFLUPhX5Fgo67Jf5
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@ChuckBuilds, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 50 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 443d610d-f298-4b40-88c7-2219a69211ce

📥 Commits

Reviewing files that changed from the base of the PR and between 2add759 and 56dbfe6.

📒 Files selected for processing (4)
  • src/cache_manager.py
  • src/display_controller.py
  • src/startup_validator.py
  • test/test_cache_cleanup_thread_ownership.py

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codacy-production

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 4 complexity · 0 duplication

Metric Results
Complexity 4
Duplication 0

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@ChuckBuilds

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@ChuckBuilds
ChuckBuilds merged commit 7171e6c into main Aug 12, 2026
9 checks passed
@ChuckBuilds
ChuckBuilds deleted the fix/one-cleanup-thread-per-cache-dir branch August 12, 2026 12:43
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.

1 participant