Complete TASK-053 snapshot read fast path
This commit is contained in:
@@ -0,0 +1,74 @@
|
|||||||
|
# TASK-053-historical-snapshots-read-only-fast-path
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
Asegurar que los endpoints de snapshots históricos se comportan como una capa de lectura rápida y no intentan recomponer o regenerar snapshots costosos durante la solicitud del usuario.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
La página histórica debe sentirse rápida. Para conseguirlo, los snapshots deben existir previamente y leerse de forma casi inmediata. Si una petición a la UI termina provocando una recomposición, una regeneración parcial o una ruta de fallback costosa, el tiempo percibido se degrada mucho. La generación debe vivir en la capa de bootstrap/runner/scheduler, no en la interacción del usuario.
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
1. Revisar la implementación actual de los endpoints de snapshots históricos y la lógica de payload para:
|
||||||
|
- resumen
|
||||||
|
- weekly leaderboard
|
||||||
|
- partidas recientes
|
||||||
|
2. Identificar si alguna de esas rutas intenta recomponer, regenerar o recalcular snapshots costosos durante la petición.
|
||||||
|
3. Corregir el comportamiento para que los endpoints de snapshots:
|
||||||
|
- lean snapshots ya existentes
|
||||||
|
- respondan rápido
|
||||||
|
- devuelvan metadatos claros si el snapshot no está listo
|
||||||
|
- no bloqueen la solicitud por regeneración pesada
|
||||||
|
4. Mantener el sistema de generación y refresco de snapshots en la capa operativa adecuada:
|
||||||
|
- bootstrap
|
||||||
|
- refresh
|
||||||
|
- runner periódico
|
||||||
|
5. Asegurar que cuando un snapshot no exista, la respuesta sea rápida y clara, sin dejar al usuario esperando recomposición pesada.
|
||||||
|
6. Documentar el contrato operativo esperado: lectura rápida en request path, generación fuera del request path.
|
||||||
|
7. No cambiar la UI en esta task salvo lo estrictamente necesario para reflejar metadatos ya existentes.
|
||||||
|
8. Al completar la implementación:
|
||||||
|
- dejar el repositorio consistente
|
||||||
|
- hacer commit
|
||||||
|
- hacer push al remoto si el entorno lo permite
|
||||||
|
|
||||||
|
## Files to Read First
|
||||||
|
- AGENTS.md
|
||||||
|
- backend/README.md
|
||||||
|
- backend/app/routes.py
|
||||||
|
- backend/app/payloads.py
|
||||||
|
- backend/app/historical_snapshot_storage.py
|
||||||
|
- backend/app/historical_snapshots.py
|
||||||
|
- backend/app/historical_runner.py
|
||||||
|
- backend/app/historical_ingestion.py
|
||||||
|
|
||||||
|
## Expected Files to Modify
|
||||||
|
- backend/app/routes.py
|
||||||
|
- backend/app/payloads.py
|
||||||
|
- backend/app/historical_snapshot_storage.py
|
||||||
|
- backend/app/historical_snapshots.py
|
||||||
|
- backend/README.md
|
||||||
|
- opcionalmente documentación técnica adicional si hace falta dejar la política clara
|
||||||
|
|
||||||
|
## Constraints
|
||||||
|
- No usar A2S para esta capa.
|
||||||
|
- No volver a agregados on-demand pesados como estrategia principal.
|
||||||
|
- No crear páginas nuevas.
|
||||||
|
- No hacer cambios destructivos.
|
||||||
|
- Mantener el trabajo centrado en un fast read path para snapshots.
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
- Los endpoints de snapshots responden como lectura rápida.
|
||||||
|
- No hay recomposición pesada o regeneración costosa en el request path del usuario.
|
||||||
|
- Si un snapshot no existe, la respuesta es rápida y clara.
|
||||||
|
- La documentación deja clara la política operativa.
|
||||||
|
- Los cambios quedan committeados y se hace push si el entorno lo permite.
|
||||||
|
|
||||||
|
## Change Budget
|
||||||
|
- Preferir menos de 5 archivos modificados o creados.
|
||||||
|
- Preferir menos de 220 líneas cambiadas.
|
||||||
|
|
||||||
|
## Outcome
|
||||||
|
- Los endpoints `/api/historical/snapshots/*` ya no intentan recomponer ni regenerar snapshots durante la petición del usuario.
|
||||||
|
- Cuando un snapshot no existe, la API responde rápido con metadata explícita de fast path: `snapshot_status`, `missing_reason`, `request_path_policy` y `generation_policy`.
|
||||||
|
- La política operativa queda documentada en `backend/README.md`: lectura rápida en request path y generación solo en `historical_ingestion` o `historical_runner`.
|
||||||
|
- Validación local:
|
||||||
|
- import y ejecución de `build_historical_server_summary_snapshot_payload(server_slug='comunidad-hispana-01')`
|
||||||
|
- `py_compile` sobre `backend/app/payloads.py`, `backend/app/routes.py` y `backend/app/historical_snapshot_storage.py`
|
||||||
@@ -523,6 +523,10 @@ precalculados bajo `backend/data/snapshots/` y evita recalcular agregados
|
|||||||
pesados en cada request. Estos endpoints devuelven payloads ligeros listos para
|
pesados en cada request. Estos endpoints devuelven payloads ligeros listos para
|
||||||
frontend con:
|
frontend con:
|
||||||
|
|
||||||
|
- `snapshot_status`
|
||||||
|
- `missing_reason`
|
||||||
|
- `request_path_policy`
|
||||||
|
- `generation_policy`
|
||||||
- `generated_at`
|
- `generated_at`
|
||||||
- `source_range_start`
|
- `source_range_start`
|
||||||
- `source_range_end`
|
- `source_range_end`
|
||||||
@@ -539,11 +543,12 @@ frontend con:
|
|||||||
- `previous_week_closed_matches`
|
- `previous_week_closed_matches`
|
||||||
- `sufficient_sample`
|
- `sufficient_sample`
|
||||||
|
|
||||||
Si un servidor ya tiene historico bruto en `historical_*` pero aun no conserva
|
Si un snapshot todavia no existe en `backend/data/snapshots/`, la API responde
|
||||||
el archivo precalculado correspondiente en `backend/data/snapshots/`, la API
|
rapido con `found: false`, `snapshot_status: "missing"` y
|
||||||
intenta regenerar automaticamente el lote de snapshots de ese servidor antes de
|
`missing_reason: "snapshot-not-generated"`. La generacion y refresco de esos
|
||||||
responder. Esto evita que un servidor quede bloqueado en `found: false` por una
|
artefactos debe ocurrir fuera del request path mediante `historical_ingestion`
|
||||||
ausencia puntual de persistencia precalculada.
|
o `historical_runner`; la lectura HTTP se mantiene como fast path de solo
|
||||||
|
lectura.
|
||||||
|
|
||||||
`/api/historical/snapshots/server-summary` devuelve `item` con el resumen del
|
`/api/historical/snapshots/server-summary` devuelve `item` con el resumen del
|
||||||
servidor. `/api/historical/snapshots/weekly-leaderboard` devuelve `items` ya
|
servidor. `/api/historical/snapshots/weekly-leaderboard` devuelve `items` ya
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ from .historical_snapshots import (
|
|||||||
SNAPSHOT_TYPE_RECENT_MATCHES,
|
SNAPSHOT_TYPE_RECENT_MATCHES,
|
||||||
SNAPSHOT_TYPE_SERVER_SUMMARY,
|
SNAPSHOT_TYPE_SERVER_SUMMARY,
|
||||||
SNAPSHOT_TYPE_WEEKLY_LEADERBOARD,
|
SNAPSHOT_TYPE_WEEKLY_LEADERBOARD,
|
||||||
generate_and_persist_historical_snapshots,
|
|
||||||
)
|
)
|
||||||
from .historical_storage import (
|
from .historical_storage import (
|
||||||
ALL_SERVERS_SLUG,
|
ALL_SERVERS_SLUG,
|
||||||
@@ -465,21 +464,6 @@ def _get_historical_snapshot_record(
|
|||||||
) -> dict[str, object] | None:
|
) -> dict[str, object] | None:
|
||||||
if not server_key:
|
if not server_key:
|
||||||
return None
|
return None
|
||||||
snapshot = get_historical_snapshot(
|
|
||||||
server_key=server_key,
|
|
||||||
snapshot_type=snapshot_type,
|
|
||||||
metric=metric,
|
|
||||||
window=window,
|
|
||||||
)
|
|
||||||
if snapshot is not None:
|
|
||||||
return snapshot
|
|
||||||
|
|
||||||
# Self-heal missing precomputed rows when raw historical data already exists.
|
|
||||||
try:
|
|
||||||
generate_and_persist_historical_snapshots(server_key=server_key)
|
|
||||||
except Exception:
|
|
||||||
return None
|
|
||||||
|
|
||||||
return get_historical_snapshot(
|
return get_historical_snapshot(
|
||||||
server_key=server_key,
|
server_key=server_key,
|
||||||
snapshot_type=snapshot_type,
|
snapshot_type=snapshot_type,
|
||||||
@@ -491,6 +475,10 @@ def _get_historical_snapshot_record(
|
|||||||
def _build_historical_snapshot_metadata(snapshot: dict[str, object] | None) -> dict[str, object]:
|
def _build_historical_snapshot_metadata(snapshot: dict[str, object] | None) -> dict[str, object]:
|
||||||
if snapshot is None:
|
if snapshot is None:
|
||||||
return {
|
return {
|
||||||
|
"snapshot_status": "missing",
|
||||||
|
"missing_reason": "snapshot-not-generated",
|
||||||
|
"request_path_policy": "read-only-fast-path",
|
||||||
|
"generation_policy": "out-of-band-refresh-only",
|
||||||
"generated_at": None,
|
"generated_at": None,
|
||||||
"source_range_start": None,
|
"source_range_start": None,
|
||||||
"source_range_end": None,
|
"source_range_end": None,
|
||||||
@@ -499,6 +487,10 @@ def _build_historical_snapshot_metadata(snapshot: dict[str, object] | None) -> d
|
|||||||
}
|
}
|
||||||
is_stale = bool(snapshot.get("is_stale", False))
|
is_stale = bool(snapshot.get("is_stale", False))
|
||||||
return {
|
return {
|
||||||
|
"snapshot_status": "ready",
|
||||||
|
"missing_reason": None,
|
||||||
|
"request_path_policy": "read-only-fast-path",
|
||||||
|
"generation_policy": "out-of-band-refresh-only",
|
||||||
"generated_at": snapshot.get("generated_at"),
|
"generated_at": snapshot.get("generated_at"),
|
||||||
"source_range_start": snapshot.get("source_range_start"),
|
"source_range_start": snapshot.get("source_range_start"),
|
||||||
"source_range_end": snapshot.get("source_range_end"),
|
"source_range_end": snapshot.get("source_range_end"),
|
||||||
|
|||||||
Reference in New Issue
Block a user