[AZ-308] c6 CacheBudgetEnforcer: 10 GB hard cap + LRU sweep

CacheBudgetEnforcer.reserve_headroom(needed_bytes) returns immediately
when total_disk_bytes() + needed_bytes <= budget, otherwise iterates
lru_candidates in eviction_batch_size batches, deletes via delete_tile,
emits one INFO log per evicted tile (c6.evicted) and one FDR record per
eviction batch (c6.eviction_batch, evicted_tile_ids capped to 5).
Raises CacheBudgetExhaustedError AFTER a full sweep if the budget
cannot be met. BudgetEnforcedTileStore decorates a TileStore so the
policy stays separable from PostgresFilesystemStore. Composition root
in storage_factory.build_tile_store wires the wrapper unconditionally.

PostgresFilesystemStore now accepts lru_clock: Clock | None = None;
when set, read_tile_pixels calls record_lru_access(tile_id, now) so
eviction picks the right LRU candidates. Production wiring injects
WallClock(); AZ-305 unit tests still construct without the clock and
keep their pass-through semantics. Contract tile_store.md bumped to
v1.1.0 to add CacheBudgetExhaustedError to the TileCacheError family;
shared FDR schema bumped to v1.3.0 for the new c6.eviction_batch kind.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-12 20:37:41 +03:00
parent 39ff47087f
commit d571ca25f9
13 changed files with 1588 additions and 29 deletions
@@ -71,14 +71,26 @@ def build_tile_store(config: Config) -> TileStore:
invoked via ``PostgresFilesystemStore.from_config(config)`` which
wires the ``ConnectionPool`` / ``FdrClient`` / logger / static
helper dependencies from the config block.
AZ-308: the returned :class:`TileStore` is wrapped in a
:class:`BudgetEnforcedTileStore` so every ``write_tile`` first
reserves head-room against the configured
``lru_eviction_threshold_bytes`` budget (RESTRICT-SAT-2). The
wrapper is transparent for read-side consumers.
"""
block = _c6_config(config)
runtime = block.store_runtime
if runtime == "postgres_filesystem":
try:
from gps_denied_onboard.components.c6_tile_cache.cache_budget_enforcer import (
BudgetEnforcedTileStore,
CacheBudgetEnforcer,
)
from gps_denied_onboard.components.c6_tile_cache.postgres_filesystem_store import (
PostgresFilesystemStore,
)
from gps_denied_onboard.fdr_client.client import make_fdr_client
from gps_denied_onboard.logging import get_logger
except ModuleNotFoundError as exc:
raise RuntimeNotAvailableError(
f"TileStore runtime {runtime!r} is configured but its "
@@ -86,7 +98,15 @@ def build_tile_store(config: Config) -> TileStore:
"'c6_tile_cache.postgres_filesystem_store' has not been "
"built into this binary yet (AZ-305 pending)."
) from exc
return PostgresFilesystemStore.from_config(config)
store = PostgresFilesystemStore.from_config(config)
enforcer = CacheBudgetEnforcer(
store=store,
fdr_client=make_fdr_client("c6_tile_cache.budget", config),
logger=get_logger("c6_tile_cache.budget"),
budget_bytes=block.lru_eviction_threshold_bytes,
eviction_batch_size=block.eviction_batch_size,
)
return BudgetEnforcedTileStore(wrapped=store, enforcer=enforcer)
raise RuntimeNotAvailableError(
f"TileStore runtime {runtime!r} is not buildable in this binary."
)