perf(api): TTL-cache /stats + unfiltered pagination counts

Every /stats call ran SELECT count(*) FROM logs + SELECT count(DISTINCT
attacker_ip) FROM logs; every /logs and /attackers call ran an
unfiltered count for the paginator. At 500 concurrent users these
serialize through aiosqlite's worker threads and dominate wall time.

Cache at the router layer (repo stays dialect-agnostic):
  - /stats response: 5s TTL
  - /logs total (only when no filters): 2s TTL
  - /attackers total (only when no filters): 2s TTL

Filtered paths bypass the cache. Pattern reused from api_get_config
and api_get_health (asyncio.Lock + time.monotonic window + lazy lock).
This commit is contained in:
2026-04-17 19:09:15 -04:00
parent de4b64d857
commit 6301504c0e
6 changed files with 233 additions and 4 deletions

View File

@@ -56,8 +56,14 @@ async def setup_db(monkeypatch) -> AsyncGenerator[None, None]:
# Reset per-request TTL caches so they don't leak across tests
from decnet.web.router.health import api_get_health as _h
from decnet.web.router.config import api_get_config as _c
from decnet.web.router.stats import api_get_stats as _s
from decnet.web.router.logs import api_get_logs as _l
from decnet.web.router.attackers import api_get_attackers as _a
_h._reset_db_cache()
_c._reset_state_cache()
_s._reset_stats_cache()
_l._reset_total_cache()
_a._reset_total_cache()
# Create schema
async with engine.begin() as conn: