Store caching¶
The Store can optionally use a second backend as a local cache for selected
namespaces, which is especially useful when the primary backend is remote,
slower or otherwise more “expensive” than the cache.
Configuration¶
cache_urlorcache_backend: where cached data is storedconfig: mapping of namespace to its configuration dict, containing nesting levels and cache policy settings.
Each namespace configuration dictionary can have:
levels: a required list of integers specifying nesting levels.cache: optional cache mode, acceptingCacheModevalues or string aliases:CacheMode.C_OFFor"off": bypass cache completely (default).CacheMode.C_MIRRORor"mirror": always read from primary backend, but update the cache after successful primary backend reads and writes.CacheMode.C_WRITETHROUGHor"writethrough": read-through + write-through. For now, only content-hash addressed namespaces should use this mode.
max_age: optional maximum age expressed in seconds since last access. The default isNone(no age limit).size: optional maximum size in bytes. It sets a per-namespace cache size budget enforced by evicting least-recently-used items until the namespace total size is within the configured budget. Items bigger thansizeare not cached.
Example:
from borgstore.store import Store, CacheMode
store = Store(
url="sftp://user@host/repo",
config={
"data": {
"levels": [2],
"cache": "writethrough",
"max_age": 3600,
"size": 4 * 1024**3,
},
"meta": {
"levels": [1],
"cache": CacheMode.C_MIRROR,
},
},
cache_url="file:///home/user/.cache/borgstore/repo",
)
Behavior¶
Cache keys are identical to primary backend keys (same nesting).
Soft-deleted items are cached under the same
.delname as primary.Soft delete/undelete renames cache entries as well.
Cache failures are non-fatal and logged as warnings.
Eviction¶
For each namespace that has a max_age or size limit, the Store keeps
an in-memory index of the cached items (name, size, time of last use) while it
is opened. The index is ordered by the last use by this store: a cache hit,
putting an item into the cache or moving it counts as using it.
On
Store.open()andStore.close(), the namespace is scanned (listed) and cleaned up. When scanning, items the store did not know yet are ordered by theirItemInfo.atime.Before an item is put into the cache (by
store()or by aload()that was a cache miss), room is made for it, so the namespace total size stays<= sizealso while the store is in use.
Cleanup order per namespace is:
remove expired cache objects when
max_ageis configured,if
sizeis configured, evict the least-recently-used remaining items until the namespace total size (plus the size of the item to put into the cache) is<= size.
Expired entries are always removed first, even if total size is already below
the size limit. A cache hit never expires anything: an expired item that is
still in the cache is served (and that counts as using it).
Manual Cache Invalidation¶
If you need to programmatically clear or invalidate parts of the cache (for
example, to resolve stale objects after primary backend deletes by other
clients, or if cache corruption is suspected), you can use the
cache_invalidate method:
To invalidate a single item:
store.cache_invalidate("data/00000000")
To invalidate all cached items in a specific namespace (e.g.
"data/"):store.cache_invalidate("data/")
To invalidate all cached items across all configured namespaces, pass
ROOTNS:from borgstore.constants import ROOTNS store.cache_invalidate(ROOTNS)
Limitations¶
No proactive cache validation/revalidation.
If an object is deleted in the primary backend by another client, the local cache will still have a stale object.
For items a
Storehas not used itself since it was opened (items cached in a previous session or by another client),max_ageand LRU-by-sizedepend on the timestamps the cache backend gives inItemInfo:atime(posixfs,sftpandRESTbackends): filesystems often do not update the atime for each read (e.g.relatimeornoatimemounts), so it can be older than the real last use.If
atimeis 0 (not implemented),mtimeis used instead (s3backend). That is the time when the item was put into the cache, somax_agerefers to that time andsizeevicts the items first that were cached first.If
mtimeis 0 also (rclonebackend), usingmax_agewould remove these items from the cache when it is scanned and usingsizewould not evict these items in LRU order, because their order can’t be determined.
If a partial range
loadcall for an object in a cached namespace causes a cache miss, the full object will be read from the primary backend and the cache will be populated with the full object (if it is not bigger thansize).
Statistics¶
Store.stats includes cache counters:
backend_load_volumebackend_store_volumebackend_load_callsbackend_store_callsbackend_delete_callscache_disabledcache_hitscache_missescache_hit_ratiocache_errorscache_load_volumecache_store_volumecache_load_callscache_store_callscache_delete_calls