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_url`` or ``cache_backend``: where cached data is stored
- ``config``: 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, accepting ``CacheMode`` values or string
  aliases:

  - ``CacheMode.C_OFF`` or ``"off"``: bypass cache completely (default).
  - ``CacheMode.C_MIRROR`` or ``"mirror"``: always read from primary backend,
    but update the cache after successful primary backend reads and writes.
  - ``CacheMode.C_WRITETHROUGH`` or ``"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 is ``None`` (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.

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 ``.del`` name as primary.
- Soft delete/undelete renames cache entries as well.
- On ``Store.open()`` and ``Store.close()``, cache-enabled namespaces are scanned
  to clean up the cache. Cleanup order per namespace is:

  1. remove expired cache objects when ``max_age`` is configured,
  2. if ``size`` is configured, evict the least-recently-used remaining items
     until the namespace total size is ``<= size``.

  Expired entries are always removed first, even if total size is already below
  the ``size`` limit.
- Cache failures are non-fatal and logged as warnings.

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
-----------

- Eviction by ``max_age`` or ``size`` is open-time and close-time only
  (``Store.open()`` / ``Store.close()``), not continuous during
  ``store()``/``load()`` operations.
- 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.
- ``max_age`` and LRU-by-``size`` depend on backend ``ItemInfo.atime`` support,
  currently that is supported by ``posixfs`` and ``REST`` backends.
  If ``atime`` is 0 (not implemented):

  - using ``max_age`` would empty the cache on ``Store.open()`` or ``Store.close()``
  - using ``size`` would not work in LRU order, because order can't be
    determined
- If a partial range ``load`` call 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.

Statistics
----------

``Store.stats`` includes cache counters:

- ``backend_load_volume``
- ``backend_store_volume``
- ``backend_load_calls``
- ``backend_store_calls``
- ``backend_delete_calls``
- ``cache_disabled``
- ``cache_hits``
- ``cache_misses``
- ``cache_hit_ratio``
- ``cache_errors``
- ``cache_load_volume``
- ``cache_store_volume``
- ``cache_load_calls``
- ``cache_store_calls``
- ``cache_delete_calls``
