Backend: rate limiter, security headers, blockchain cache service avec RPC, public API (7 endpoints read-only), WebSocket auth + heartbeat, DB connection pooling, structured logging, health check DB. Frontend: API retry/timeout, WebSocket auth + heartbeat + typed events, notifications toast, mobile hamburger + drawer, error boundary, offline banner, loading skeletons, dashboard enrichi. Documentation: guides utilisateur complets (demarrage, vote, sanctuaire, FAQ 30+), guide deploiement, politique securite. 123 tests, 155 fichiers. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
141 lines
3.4 KiB
Python
141 lines
3.4 KiB
Python
"""Cache service: blockchain data caching with TTL expiry.
|
|
|
|
Uses the BlockchainCache model (PostgreSQL/JSONB) to cache
|
|
on-chain data like WoT size, Smith size, and TechComm size.
|
|
Avoids repeated RPC calls to the Duniter node.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from datetime import datetime, timedelta, timezone
|
|
|
|
from sqlalchemy import delete, select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.models.cache import BlockchainCache
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
async def get_cached(key: str, db: AsyncSession) -> dict | None:
|
|
"""Retrieve a cached value by key if it has not expired.
|
|
|
|
Parameters
|
|
----------
|
|
key:
|
|
The cache key to look up.
|
|
db:
|
|
Async database session.
|
|
|
|
Returns
|
|
-------
|
|
dict | None
|
|
The cached value as a dict, or None if missing/expired.
|
|
"""
|
|
result = await db.execute(
|
|
select(BlockchainCache).where(
|
|
BlockchainCache.cache_key == key,
|
|
BlockchainCache.expires_at > datetime.now(timezone.utc),
|
|
)
|
|
)
|
|
entry = result.scalar_one_or_none()
|
|
|
|
if entry is None:
|
|
logger.debug("Cache miss pour la cle '%s'", key)
|
|
return None
|
|
|
|
logger.debug("Cache hit pour la cle '%s'", key)
|
|
return entry.cache_value
|
|
|
|
|
|
async def set_cached(
|
|
key: str,
|
|
value: dict,
|
|
db: AsyncSession,
|
|
ttl_seconds: int = 3600,
|
|
) -> None:
|
|
"""Store a value in the cache with the given TTL.
|
|
|
|
If the key already exists, it is replaced (upsert).
|
|
|
|
Parameters
|
|
----------
|
|
key:
|
|
The cache key.
|
|
value:
|
|
The value to store (must be JSON-serializable).
|
|
db:
|
|
Async database session.
|
|
ttl_seconds:
|
|
Time-to-live in seconds (default: 1 hour).
|
|
"""
|
|
now = datetime.now(timezone.utc)
|
|
expires_at = now + timedelta(seconds=ttl_seconds)
|
|
|
|
# Check if key already exists
|
|
result = await db.execute(
|
|
select(BlockchainCache).where(BlockchainCache.cache_key == key)
|
|
)
|
|
existing = result.scalar_one_or_none()
|
|
|
|
if existing is not None:
|
|
existing.cache_value = value
|
|
existing.fetched_at = now
|
|
existing.expires_at = expires_at
|
|
logger.debug("Cache mis a jour pour la cle '%s' (TTL=%ds)", key, ttl_seconds)
|
|
else:
|
|
entry = BlockchainCache(
|
|
cache_key=key,
|
|
cache_value=value,
|
|
fetched_at=now,
|
|
expires_at=expires_at,
|
|
)
|
|
db.add(entry)
|
|
logger.debug("Cache cree pour la cle '%s' (TTL=%ds)", key, ttl_seconds)
|
|
|
|
await db.commit()
|
|
|
|
|
|
async def invalidate(key: str, db: AsyncSession) -> None:
|
|
"""Remove a specific cache entry by key.
|
|
|
|
Parameters
|
|
----------
|
|
key:
|
|
The cache key to invalidate.
|
|
db:
|
|
Async database session.
|
|
"""
|
|
await db.execute(
|
|
delete(BlockchainCache).where(BlockchainCache.cache_key == key)
|
|
)
|
|
await db.commit()
|
|
logger.debug("Cache invalide pour la cle '%s'", key)
|
|
|
|
|
|
async def cleanup_expired(db: AsyncSession) -> int:
|
|
"""Remove all expired cache entries.
|
|
|
|
Parameters
|
|
----------
|
|
db:
|
|
Async database session.
|
|
|
|
Returns
|
|
-------
|
|
int
|
|
Number of entries removed.
|
|
"""
|
|
result = await db.execute(
|
|
delete(BlockchainCache).where(
|
|
BlockchainCache.expires_at <= datetime.now(timezone.utc)
|
|
)
|
|
)
|
|
await db.commit()
|
|
|
|
count = result.rowcount
|
|
if count > 0:
|
|
logger.info("Nettoyage cache: %d entrees expirees supprimees", count)
|
|
return count
|