fix(api): retain ready asset matrix on timeout

This commit is contained in:
ogt
2026-07-15 07:21:00 +08:00
parent fc81693c64
commit a088bcc4ea
2 changed files with 269 additions and 2 deletions

View File

@@ -534,3 +534,107 @@ def test_matrix_build_phase_is_inside_the_total_timeout(monkeypatch: Any) -> Non
assert payload["live_readback_status"] == "degraded"
assert payload["live_readback_error_type"] == "TimeoutError"
assert payload["live_readback_timeout_seconds"] == 0.01
def test_public_matrix_returns_fresh_cache_without_live_db_read(
monkeypatch: Any,
) -> None:
async def _cached_read(
_project_id: str,
*,
namespace: str,
ttl_seconds: int,
) -> dict[str, Any] | None:
assert namespace == matrix_service._PUBLIC_MATRIX_FRESH_CACHE_NAMESPACE
assert ttl_seconds == matrix_service.PUBLIC_MATRIX_FRESH_CACHE_TTL_SECONDS
return {
"live_readback_status": "ready",
"summary": {"live_observed_asset_count": 193},
"cache": {"status": "hit", "age_seconds": 2.5},
}
async def _unexpected_live_read(
_project_id: str,
) -> tuple[dict[str, Any], list[dict[str, Any]], dict[str, Any]]:
raise AssertionError("fresh cache must bypass the live DB read")
monkeypatch.setattr(matrix_service, "_read_public_matrix_cache", _cached_read)
monkeypatch.setattr(matrix_service, "_load_live_rows", _unexpected_live_read)
payload = asyncio.run(build_asset_capability_matrix_with_live_readback())
assert payload["live_readback_status"] == "ready"
assert payload["summary"]["live_observed_asset_count"] == 193
assert payload["live_readback_cache"]["mode"] == "fresh"
assert payload["live_readback_cache"]["fallback_used"] is False
def test_public_matrix_success_warms_cache_for_next_request(
monkeypatch: Any,
) -> None:
from src.services.operator_summary_cache import clear_operator_summary_cache
live_read_count = 0
async def _live_readback(
_project_id: str,
) -> tuple[dict[str, Any], list[dict[str, Any]], dict[str, Any]]:
nonlocal live_read_count
live_read_count += 1
return {}, [], {}
clear_operator_summary_cache()
monkeypatch.setattr(matrix_service, "_load_live_rows", _live_readback)
try:
first = asyncio.run(build_asset_capability_matrix_with_live_readback())
second = asyncio.run(build_asset_capability_matrix_with_live_readback())
finally:
clear_operator_summary_cache()
assert first["live_readback_status"] == "ready"
assert second["live_readback_status"] == "ready"
assert second["live_readback_cache"]["mode"] == "fresh"
assert live_read_count == 1
def test_public_matrix_timeout_uses_last_ready_cache(monkeypatch: Any) -> None:
async def _slow_live_readback(
_project_id: str,
) -> tuple[dict[str, Any], list[dict[str, Any]], dict[str, Any]]:
await asyncio.sleep(1)
return {}, [], {}
async def _cached_read(
_project_id: str,
*,
namespace: str,
ttl_seconds: int,
) -> dict[str, Any] | None:
if namespace == matrix_service._PUBLIC_MATRIX_FRESH_CACHE_NAMESPACE:
return None
assert namespace == matrix_service._PUBLIC_MATRIX_FALLBACK_CACHE_NAMESPACE
assert ttl_seconds == matrix_service.PUBLIC_MATRIX_FALLBACK_CACHE_TTL_SECONDS
return {
"live_readback_status": "ready",
"summary": {
"live_inventory_asset_count": 9595,
"live_observed_asset_count": 193,
"stage_coverage_percent": 18,
},
"cache": {"status": "hit", "age_seconds": 51.2},
}
monkeypatch.setattr(matrix_service, "_read_public_matrix_cache", _cached_read)
monkeypatch.setattr(matrix_service, "_load_live_rows", _slow_live_readback)
payload = asyncio.run(
build_asset_capability_matrix_with_live_readback(timeout_seconds=0.01)
)
assert payload["live_readback_status"] == "degraded_cached"
assert payload["live_readback_error_type"] == "TimeoutError"
assert payload["summary"]["live_inventory_asset_count"] == 9595
assert payload["summary"]["live_observed_asset_count"] == 193
assert payload["summary"]["stage_coverage_percent"] == 18
assert payload["live_readback_cache"]["mode"] == "stale_if_error"
assert payload["live_readback_cache"]["fallback_used"] is True