mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-21 10:11:12 +00:00
[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:
@@ -56,13 +56,9 @@ from gps_denied_onboard.runtime_root.storage_factory import (
|
||||
build_tile_store,
|
||||
)
|
||||
|
||||
_CONTRACT_DIR = Path(__file__).resolve().parents[3] / (
|
||||
"_docs/02_document/contracts/c6_tile_cache"
|
||||
)
|
||||
_CONTRACT_DIR = Path(__file__).resolve().parents[3] / ("_docs/02_document/contracts/c6_tile_cache")
|
||||
_FAKE_IMPL_MODULE = "gps_denied_onboard.components.c6_tile_cache.faiss_descriptor_index"
|
||||
_FAKE_STORE_MODULE = (
|
||||
"gps_denied_onboard.components.c6_tile_cache.postgres_filesystem_store"
|
||||
)
|
||||
_FAKE_STORE_MODULE = "gps_denied_onboard.components.c6_tile_cache.postgres_filesystem_store"
|
||||
|
||||
|
||||
def _valid_tile_id(zoom: int = 18, lat: float = 49.94, lon: float = 36.31) -> TileId:
|
||||
@@ -320,6 +316,15 @@ def _install_fake_postgres_store_module() -> type:
|
||||
# preserves the single-config-arg shape via this classmethod.
|
||||
return cls(config)
|
||||
|
||||
# AZ-308: ``build_tile_store`` now wraps the store in a
|
||||
# ``BudgetEnforcedTileStore`` whose constructor reads
|
||||
# ``total_disk_bytes`` for the AC-12 startup log. Override the
|
||||
# ``_FullTileMetadataStore`` NotImplementedError stub with a
|
||||
# working zero-byte response so the factory can construct the
|
||||
# wrapper without touching a real DB.
|
||||
def total_disk_bytes(self) -> int:
|
||||
return 0
|
||||
|
||||
fake_module = types.ModuleType(_FAKE_STORE_MODULE)
|
||||
fake_module.PostgresFilesystemStore = _FakePostgresFilesystemStore # type: ignore[attr-defined]
|
||||
sys.modules[_FAKE_STORE_MODULE] = fake_module
|
||||
@@ -349,11 +354,21 @@ def test_ac5_build_descriptor_index_flag_off_raises_no_import(
|
||||
|
||||
|
||||
def test_ac4_build_tile_store_returns_protocol_impl(store_module_cleanup) -> None:
|
||||
# AZ-308: ``build_tile_store`` now returns a ``BudgetEnforcedTileStore``
|
||||
# decorator wrapping the inner :class:`TileStore` impl. The decorator
|
||||
# implements the Protocol surface; the wrapped instance is reachable
|
||||
# via the private ``_wrapped`` attribute for tests that need to
|
||||
# introspect the inner store.
|
||||
from gps_denied_onboard.components.c6_tile_cache.cache_budget_enforcer import (
|
||||
BudgetEnforcedTileStore,
|
||||
)
|
||||
|
||||
fake_cls = _install_fake_postgres_store_module()
|
||||
config = _config_with_c6()
|
||||
store = build_tile_store(config)
|
||||
assert isinstance(store, fake_cls)
|
||||
assert isinstance(store, BudgetEnforcedTileStore)
|
||||
assert isinstance(store, TileStore)
|
||||
assert isinstance(store._wrapped, fake_cls) # type: ignore[attr-defined]
|
||||
|
||||
|
||||
def test_ac4_build_tile_metadata_store_returns_protocol_impl(
|
||||
@@ -366,9 +381,7 @@ def test_ac4_build_tile_metadata_store_returns_protocol_impl(
|
||||
assert isinstance(md, TileMetadataStore)
|
||||
|
||||
|
||||
def test_ac5_tile_store_runtime_module_missing_raises(
|
||||
store_module_cleanup, monkeypatch
|
||||
) -> None:
|
||||
def test_ac5_tile_store_runtime_module_missing_raises(store_module_cleanup, monkeypatch) -> None:
|
||||
"""AC-5 historical name; after AZ-305 the impl module always exists, so
|
||||
"missing" is exercised by deleting it from ``sys.modules`` AND making
|
||||
``importlib`` refuse the import. We patch the module-level lazy import
|
||||
@@ -378,14 +391,18 @@ def test_ac5_tile_store_runtime_module_missing_raises(
|
||||
config = _config_with_c6()
|
||||
import gps_denied_onboard.runtime_root.storage_factory as factory_mod
|
||||
|
||||
real_import = __builtins__["__import__"] if isinstance(__builtins__, dict) else __builtins__.__import__
|
||||
real_import = (
|
||||
__builtins__["__import__"] if isinstance(__builtins__, dict) else __builtins__.__import__
|
||||
)
|
||||
|
||||
def _block_postgres_import(name, *args, **kwargs):
|
||||
if name.endswith("postgres_filesystem_store"):
|
||||
raise ModuleNotFoundError(name)
|
||||
return real_import(name, *args, **kwargs)
|
||||
|
||||
monkeypatch.setattr(factory_mod, "__builtins__", {"__import__": _block_postgres_import}, raising=False)
|
||||
monkeypatch.setattr(
|
||||
factory_mod, "__builtins__", {"__import__": _block_postgres_import}, raising=False
|
||||
)
|
||||
monkeypatch.setitem(sys.modules, _FAKE_STORE_MODULE, None) # type: ignore[arg-type]
|
||||
with pytest.raises(RuntimeNotAvailableError) as exc_info:
|
||||
build_tile_store(config)
|
||||
@@ -428,9 +445,7 @@ def test_ac6_unknown_metadata_runtime_rejected() -> None:
|
||||
({"zoom_level": 18, "lat": 0.0, "lon": -200.0}, "lon"),
|
||||
],
|
||||
)
|
||||
def test_ac7_tile_id_rejects_bad_input(
|
||||
kwargs: dict[str, float], offending_field: str
|
||||
) -> None:
|
||||
def test_ac7_tile_id_rejects_bad_input(kwargs: dict[str, float], offending_field: str) -> None:
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
TileId(**kwargs) # type: ignore[arg-type]
|
||||
assert offending_field in str(exc_info.value)
|
||||
@@ -504,9 +519,7 @@ def _methods_from_contract(contract_file: Path) -> set[str]:
|
||||
def _protocol_methods(proto: type) -> set[str]:
|
||||
"""Reflect over a Protocol's method names."""
|
||||
return {
|
||||
name
|
||||
for name in dir(proto)
|
||||
if not name.startswith("_") and callable(getattr(proto, name))
|
||||
name for name in dir(proto) if not name.startswith("_") and callable(getattr(proto, name))
|
||||
}
|
||||
|
||||
|
||||
@@ -518,9 +531,7 @@ def _protocol_methods(proto: type) -> set[str]:
|
||||
("descriptor_index.md", DescriptorIndex),
|
||||
],
|
||||
)
|
||||
def test_ac9_contract_methods_match_protocol(
|
||||
contract_filename: str, proto: type
|
||||
) -> None:
|
||||
def test_ac9_contract_methods_match_protocol(contract_filename: str, proto: type) -> None:
|
||||
contract_path = _CONTRACT_DIR / contract_filename
|
||||
contract_methods = _methods_from_contract(contract_path)
|
||||
protocol_methods = _protocol_methods(proto)
|
||||
|
||||
Reference in New Issue
Block a user