fix: keep backend startup independent from paused elo
Avoid eager Elo/MMR imports during backend startup while the feature is paused. Add validation coverage for backend startup and health route resolution.
This commit is contained in:
175
ai/tasks/done/TASK-109-fix-backend-startup-with-elo-paused.md
Normal file
175
ai/tasks/done/TASK-109-fix-backend-startup-with-elo-paused.md
Normal file
@@ -0,0 +1,175 @@
|
||||
---
|
||||
id: TASK-109
|
||||
title: Keep backend startup independent from paused Elo/MMR
|
||||
status: pending
|
||||
type: backend
|
||||
team: Backend Senior
|
||||
supporting_teams: ["Arquitecto Python"]
|
||||
roadmap_item: foundation
|
||||
priority: high
|
||||
---
|
||||
|
||||
# TASK-109 - Keep backend startup independent from paused Elo/MMR
|
||||
|
||||
## Goal
|
||||
|
||||
Fix backend startup so the default operational Compose profile can start `backend` and `frontend` while Elo/MMR and complex historical materialization remain paused.
|
||||
|
||||
The backend must expose `/health` and non-Elo routes even if Elo/MMR engine internals are unavailable or temporarily broken.
|
||||
|
||||
## Context
|
||||
|
||||
HLL Vietnam has simplified its default operational mode. The normal Compose profile should start only `backend` and `frontend`; historical workers, Elo/MMR, complex historical materialization, and server #03 are paused operationally.
|
||||
|
||||
Current validation shows:
|
||||
|
||||
- `docker compose config --services` returns only `backend` and `frontend`.
|
||||
- `docker compose up -d --build` starts the frontend successfully.
|
||||
- The frontend responds with HTTP 200.
|
||||
- The backend restarts continuously.
|
||||
- `/health` fails because backend startup imports Elo/MMR code.
|
||||
- Backend logs include: `ImportError: cannot import name 'ELO_K_FACTOR' from 'app.elo_mmr_models'`.
|
||||
- The startup import path is: `app.main -> app.routes -> app.payloads -> app.elo_mmr_engine -> app.elo_mmr_models`.
|
||||
- `payloads.py` imports Elo/MMR payload functions at module import time, so a paused or broken Elo implementation can break the whole backend, including `/health`.
|
||||
|
||||
This task is startup-focused. Preserve the existing HLL Vietnam product identity and repository discipline, and keep the fix narrow.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Inspect the listed files before changing anything.
|
||||
2. Confirm the backend startup import path and where Elo/MMR is imported at module load time.
|
||||
3. Refactor only the startup boundary needed so `/health` and non-Elo routes do not depend on successful import of Elo/MMR engine internals.
|
||||
4. Prefer lazy importing Elo/MMR functions only inside Elo-specific payload builders or routes.
|
||||
5. If Elo/MMR is paused or unavailable, make Elo-specific endpoints return a controlled unavailable or fallback payload instead of crashing backend import.
|
||||
6. Preserve the public API shape as much as possible.
|
||||
7. Add or update tests if a backend test framework exists.
|
||||
8. Update lightweight validation if needed so future startup import regressions are caught.
|
||||
9. Document the paused Elo/MMR startup boundary if documentation needs adjustment.
|
||||
10. Run the required validation before committing.
|
||||
11. Move this task file to `ai/tasks/done/` only after validation is complete and the outcome is documented.
|
||||
12. Stage only intended files, commit, and push the branch to origin.
|
||||
|
||||
## Files to Read First
|
||||
|
||||
- `backend/app/main.py`
|
||||
- `backend/app/routes.py`
|
||||
- `backend/app/payloads.py`
|
||||
- `backend/app/elo_mmr_engine.py`
|
||||
- `backend/app/elo_mmr_models.py`
|
||||
- `backend/app/config.py`
|
||||
- `backend/tests/` if present
|
||||
- `docker-compose.yml`
|
||||
- `scripts/run-integration-tests.ps1`
|
||||
- `README.md`
|
||||
- `docs/decisions.md`
|
||||
|
||||
## Expected Files to Modify
|
||||
|
||||
- likely `backend/app/payloads.py`
|
||||
- possibly `backend/app/routes.py`
|
||||
- possibly `backend/app/elo_mmr_engine.py`
|
||||
- possibly `backend/app/elo_mmr_models.py` only if justified
|
||||
- possibly tests under `backend/tests/`
|
||||
- possibly `scripts/run-integration-tests.ps1`
|
||||
- this task file moved from `ai/tasks/pending/` to `ai/tasks/done/`
|
||||
|
||||
If additional files become necessary, explain why in the task outcome and commit message.
|
||||
|
||||
## Expected Files Not to Modify
|
||||
|
||||
- `frontend/**`
|
||||
- `docker-compose.yml`, unless validation proves a Compose-only issue also exists
|
||||
- database migrations
|
||||
- persisted data
|
||||
- unrelated backend modules
|
||||
- server #03 configuration
|
||||
|
||||
## Constraints
|
||||
|
||||
- Do not delete Elo/MMR code.
|
||||
- Do not delete historical ingestion code.
|
||||
- Do not remove database migrations or persisted data.
|
||||
- Do not change frontend behavior.
|
||||
- Do not reintroduce server #03.
|
||||
- Keep the fix narrow and startup-focused.
|
||||
- Do not implement a new Elo/MMR algorithm.
|
||||
- Do not reintroduce `ELO_K_FACTOR` as a blind compatibility patch unless you prove it is the smallest safe fix and still prevents startup coupling.
|
||||
- Keep `/health` available even if Elo/MMR import fails.
|
||||
- Preserve existing public API shape as much as possible.
|
||||
- Do not modify unrelated files.
|
||||
- Do not leave completed work only in local; commit and push after validation.
|
||||
|
||||
## Validation
|
||||
|
||||
Before completing the task, run and document:
|
||||
|
||||
- `git status`
|
||||
- `powershell -ExecutionPolicy Bypass -File scripts/run-integration-tests.ps1`
|
||||
- `docker compose down`
|
||||
- `docker compose up -d --build`
|
||||
- `docker compose ps`
|
||||
- `docker compose logs --tail=100 backend`
|
||||
- `Invoke-WebRequest http://localhost:8000/health | Select-Object -ExpandProperty Content`
|
||||
- `Invoke-WebRequest http://localhost:8080 | Select-Object -ExpandProperty StatusCode`
|
||||
|
||||
Also confirm and document:
|
||||
|
||||
- backend is not restarting
|
||||
- `/health` returns a valid payload
|
||||
- frontend still returns 200
|
||||
- no frontend files changed
|
||||
- no database migrations or persisted data changed
|
||||
- `backend/runtime/` is not created or committed
|
||||
- `git diff --name-only` matches the expected scope
|
||||
|
||||
If integration tests are relevant and `scripts/run-integration-tests.ps1` exists, use it. If no backend test framework exists for this scope, document that explicitly in the outcome.
|
||||
|
||||
## Commit And Push Requirements
|
||||
|
||||
- Run all validation before committing.
|
||||
- Run `git status`.
|
||||
- Stage only intended files.
|
||||
- Commit with a clear message, for example: `fix: keep backend startup independent from paused elo`.
|
||||
- Push the branch to origin.
|
||||
- Do not leave completed work only in local.
|
||||
|
||||
## Outcome
|
||||
|
||||
- Validation performed:
|
||||
- `git status --short`
|
||||
- `python -c "import sys; sys.path.insert(0, 'backend'); import app.main; from app.routes import resolve_get_payload; print(resolve_get_payload('/health'))"`
|
||||
- `python -c "import sys; sys.path.insert(0, 'backend'); from app.routes import resolve_get_payload; print(resolve_get_payload('/api/historical/elo-mmr/leaderboard'))"`
|
||||
- `powershell -ExecutionPolicy Bypass -File scripts/run-integration-tests.ps1`
|
||||
- `docker compose down`
|
||||
- `docker compose up -d --build`
|
||||
- `docker compose ps`
|
||||
- `docker compose logs --tail=100 backend`
|
||||
- `Invoke-WebRequest http://localhost:8000/health | Select-Object -ExpandProperty Content`
|
||||
- `Invoke-WebRequest http://localhost:8080 | Select-Object -ExpandProperty StatusCode`
|
||||
- `Invoke-WebRequest 'http://localhost:8000/api/historical/elo-mmr/leaderboard' | Select-Object -ExpandProperty Content`
|
||||
- `git diff --name-only`
|
||||
- Startup coupling removed:
|
||||
- `backend/app/payloads.py` no longer imports `app.elo_mmr_engine` at module load time.
|
||||
- Elo/MMR engine imports now happen only inside Elo/MMR payload builders.
|
||||
- `app.main -> app.routes -> app.payloads` can load and serve `/health` without importing paused Elo/MMR internals.
|
||||
- Elo/MMR unavailable behavior:
|
||||
- If the Elo/MMR engine cannot be imported, Elo/MMR endpoints return the existing `status` + `data` envelope with `source: "elo-mmr-paused"`, `available: false`, `unavailable_reason: "elo-mmr-engine-import-unavailable"`, normal source policy metadata, and empty/null Elo data.
|
||||
- Tests and validation:
|
||||
- No backend test framework is configured for this scope.
|
||||
- `scripts/run-integration-tests.ps1` now includes a lightweight backend startup import check and `/health` route resolution check.
|
||||
- Scope confirmation:
|
||||
- Backend was up and not restarting in `docker compose ps`.
|
||||
- `/health` returned a valid JSON payload with `status: "ok"`.
|
||||
- Frontend returned HTTP `200`.
|
||||
- No frontend files changed.
|
||||
- No database migrations, persisted data, or server #03 configuration changed.
|
||||
- `backend/runtime/` was not created.
|
||||
- `git diff --name-only` matched the expected scope: `backend/app/payloads.py` and `scripts/run-integration-tests.ps1`.
|
||||
- Commit and push:
|
||||
- Commit hash and pushed branch are recorded in the final task execution response.
|
||||
|
||||
## Change Budget
|
||||
|
||||
- Prefer fewer than 5 modified files.
|
||||
- Prefer changes under 200 lines when feasible.
|
||||
- Split follow-up work into a new task if the scope expands beyond startup independence.
|
||||
@@ -20,10 +20,6 @@ from .data_sources import (
|
||||
get_live_data_source,
|
||||
get_rcon_historical_read_model,
|
||||
)
|
||||
from .elo_mmr_engine import (
|
||||
get_elo_mmr_player_payload,
|
||||
list_elo_mmr_leaderboard_payload,
|
||||
)
|
||||
from .historical_snapshot_storage import get_historical_snapshot
|
||||
from .historical_snapshots import (
|
||||
DEFAULT_MONTHLY_SNAPSHOT_WINDOW,
|
||||
@@ -1097,6 +1093,22 @@ def build_elo_mmr_leaderboard_payload(
|
||||
server_id: str | None = None,
|
||||
) -> dict[str, object]:
|
||||
"""Return the current Elo/MMR monthly leaderboard."""
|
||||
engine = _load_elo_mmr_engine()
|
||||
if engine is None:
|
||||
return _build_elo_mmr_unavailable_payload(
|
||||
context="historical-elo-mmr-leaderboard",
|
||||
title=(
|
||||
"Leaderboard mensual Elo/MMR global"
|
||||
if server_id == ALL_SERVERS_SLUG
|
||||
else "Leaderboard mensual Elo/MMR por servidor"
|
||||
),
|
||||
server_id=server_id,
|
||||
limit=limit,
|
||||
extra={"items": []},
|
||||
operation="elo-mmr-leaderboard",
|
||||
)
|
||||
|
||||
list_elo_mmr_leaderboard_payload = engine[1]
|
||||
payload = list_elo_mmr_leaderboard_payload(server_id=server_id, limit=limit)
|
||||
is_all_servers = server_id == ALL_SERVERS_SLUG
|
||||
accuracy_contract = _build_elo_accuracy_contract(payload.get("capabilities_summary"))
|
||||
@@ -1137,6 +1149,21 @@ def build_elo_mmr_player_payload(
|
||||
server_id: str | None = None,
|
||||
) -> dict[str, object]:
|
||||
"""Return one Elo/MMR player profile."""
|
||||
engine = _load_elo_mmr_engine()
|
||||
if engine is None:
|
||||
return _build_elo_mmr_unavailable_payload(
|
||||
context="historical-elo-mmr-player",
|
||||
title="Perfil Elo/MMR de jugador",
|
||||
server_id=server_id,
|
||||
extra={
|
||||
"player_id": player_id,
|
||||
"found": False,
|
||||
"profile": None,
|
||||
},
|
||||
operation="elo-mmr-player",
|
||||
)
|
||||
|
||||
get_elo_mmr_player_payload, list_elo_mmr_leaderboard_payload = engine
|
||||
profile = get_elo_mmr_player_payload(player_id=player_id, server_id=server_id)
|
||||
source_policy = list_elo_mmr_leaderboard_payload(server_id=server_id, limit=1).get("source_policy")
|
||||
accuracy_contract = _build_elo_player_accuracy_contract(profile)
|
||||
@@ -1160,6 +1187,52 @@ def build_elo_mmr_player_payload(
|
||||
}
|
||||
|
||||
|
||||
def _load_elo_mmr_engine():
|
||||
try:
|
||||
from .elo_mmr_engine import ( # noqa: PLC0415 - lazy boundary for paused Elo/MMR
|
||||
get_elo_mmr_player_payload,
|
||||
list_elo_mmr_leaderboard_payload,
|
||||
)
|
||||
except ImportError:
|
||||
return None
|
||||
return get_elo_mmr_player_payload, list_elo_mmr_leaderboard_payload
|
||||
|
||||
|
||||
def _build_elo_mmr_unavailable_payload(
|
||||
*,
|
||||
context: str,
|
||||
title: str,
|
||||
server_id: str | None,
|
||||
operation: str,
|
||||
limit: int | None = None,
|
||||
extra: dict[str, object] | None = None,
|
||||
) -> dict[str, object]:
|
||||
accuracy_contract = _build_elo_accuracy_contract(None)
|
||||
data = {
|
||||
"title": title,
|
||||
"context": context,
|
||||
"source": "elo-mmr-paused",
|
||||
"server_slug": server_id,
|
||||
"available": False,
|
||||
"unavailable_reason": "elo-mmr-engine-import-unavailable",
|
||||
**_resolve_historical_fallback_policy(
|
||||
operation=operation,
|
||||
fallback_reason="elo-mmr-operationally-paused",
|
||||
),
|
||||
"capabilities_summary": None,
|
||||
"accuracy_contract": accuracy_contract,
|
||||
"model_contract": _build_elo_model_contract(accuracy_contract),
|
||||
}
|
||||
if limit is not None:
|
||||
data["limit"] = limit
|
||||
if extra:
|
||||
data.update(extra)
|
||||
return {
|
||||
"status": "ok",
|
||||
"data": data,
|
||||
}
|
||||
|
||||
|
||||
def _build_elo_player_accuracy_contract(profile: dict[str, object] | None) -> dict[str, object]:
|
||||
if not isinstance(profile, dict):
|
||||
return _build_elo_accuracy_contract(None)
|
||||
|
||||
@@ -42,6 +42,20 @@ if (-not (Test-Path "ai/reports/.gitkeep")) {
|
||||
throw "Missing ai/reports/.gitkeep"
|
||||
}
|
||||
|
||||
$backendImportCheck = @'
|
||||
import sys
|
||||
sys.path.insert(0, "backend")
|
||||
import app.main
|
||||
from app.routes import resolve_get_payload
|
||||
|
||||
status, payload = resolve_get_payload("/health")
|
||||
if status is None or payload.get("status") != "ok":
|
||||
raise SystemExit("Backend health route did not resolve to an ok payload.")
|
||||
'@
|
||||
|
||||
$backendImportCheck | python -
|
||||
|
||||
Write-Host "No product integration tests are configured for this platform-only scope."
|
||||
Write-Host "Backend startup import check passed."
|
||||
Write-Host "Platform validation passed."
|
||||
exit 0
|
||||
|
||||
Reference in New Issue
Block a user