caching: continuous cache eviction, fixes #183 - #223
Merged
ThomasWaldmann merged 1 commit intoSep 19, 2026
Merged
Conversation
The cache was only cleaned up by Store.open() and Store.close(), so reading a lot of data grew a size limited cache without bounds while the store was in use. For each cache namespace with a max_age or size limit, the Store now keeps an in-memory index (name, size, last use) and makes room before it puts an item into the cache. Items bigger than the size limit are not cached. The index is ordered by the last use by this store, so the LRU order does not depend on the atime for items used in this session (filesystems often only update the atime for the first read after a write). Shared caches: a store rescans a size limited namespace after it has put more than size / 4 bytes into it, so it also sees what other clients sharing the cache have added or evicted. Scanning a namespace the cache backend does not have yet is not an error. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
ThomasWaldmann
force-pushed
the
continuous-cache-eviction
branch
from
September 19, 2026 22:07
483c3fc to
e45235b
Compare
ThomasWaldmann
added a commit
that referenced
this pull request
Sep 20, 2026
caching: scan the cache in steps, mtime fallback, more tests (follow-up to #223)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #183.
Problem
The cache was only cleaned up by
Store.open()andStore.close(). While the store was in use, a cache with asizelimit grew without bounds - reading a lot of data used as much disk space as all the loaded objects together (a partialloadthat misses caches the full object).Change
max_ageorsizelimit, theStorekeeps an in-memory index (name, size, last use) while it is opened. Open and close still scan and clean up the namespace, the scan now also fills the index.store()or by aload()cache miss), room is made for it: expired items first, then least recently used items. So the namespace total stays<= sizealso while the store is in use. The per-operation cost is a dict operation, the cache backend is not listed.sizeare not cached (and a previous value of that name is removed from the cache).max_ageis only checked when something is put into the cache and at open / close.Shared caches
Several clients may share one cache (e.g. a cache directory used by multiple processes):
sizelimited namespace after it has put more thansize / 4bytes into it (CACHE_RESCAN_DIVISOR), so it sees what other clients have added or evicted. With N clients the total can temporarily reach aboutsize * (1 + N / 4). Scan cost measured on a local posixfs cache with levels[1]: 8 ms / 500 items, 35 ms / 5000 items, 317 ms / 50000 items.touchAPI), so a client might evict an item another client frequently uses.Also
Scanning a namespace the cache backend does not have yet (first open of a fresh cache) is not an error any more - it logged a "cache cleanup failed ... ObjectNotFound" warning and counted a cache error.
Tests
13 new tests in
tests/test_cache.py: the size limit holds while loading (the scenario of #183) and while storing, LRU order of this session, partial load miss, items bigger thansize, index accounting (overwrite, delete, soft delete / undelete, invalidation), three shared cache cases,max_ageeviction when storing, eviction errors, no index without limits, first open.test_close_cleans_up_lru_cache_items_by_sizeandtest_close_cleans_up_expired_before_lru_size_evictionnow fill the cache via a secondStoresharing it: they exceededsizewhile storing, which evicts right away now.Checked with borg (master,
BORG_STORE_CACHE,BORG_PACK_CACHE_SIZE=60000000, extracting 120 MB from packs of up to 51 MB): the pack cache peaks at 50.9 MB, with borgstore 0.6.2 it peaks at 117 MB.No CHANGES.rst entry yet.
🤖 Generated with Claude Code