Sprint 5 : integration et production -- securite, performance, API publique, documentation
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>
This commit is contained in:
140
backend/app/services/cache_service.py
Normal file
140
backend/app/services/cache_service.py
Normal file
@@ -0,0 +1,140 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user