From 2271608836ea54b8dd82928bb8654b80c17c9ded Mon Sep 17 00:00:00 2001 From: devRaGonSa <97627393+devRaGonSa@users.noreply.github.com> Date: Tue, 19 May 2026 09:09:20 +0200 Subject: [PATCH] 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. --- ...109-fix-backend-startup-with-elo-paused.md | 175 ++++++++++++++++++ backend/app/payloads.py | 81 +++++++- scripts/run-integration-tests.ps1 | 14 ++ 3 files changed, 266 insertions(+), 4 deletions(-) create mode 100644 ai/tasks/done/TASK-109-fix-backend-startup-with-elo-paused.md diff --git a/ai/tasks/done/TASK-109-fix-backend-startup-with-elo-paused.md b/ai/tasks/done/TASK-109-fix-backend-startup-with-elo-paused.md new file mode 100644 index 0000000..6f32c4a --- /dev/null +++ b/ai/tasks/done/TASK-109-fix-backend-startup-with-elo-paused.md @@ -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. diff --git a/backend/app/payloads.py b/backend/app/payloads.py index d03fdd0..20a35a8 100644 --- a/backend/app/payloads.py +++ b/backend/app/payloads.py @@ -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) diff --git a/scripts/run-integration-tests.ps1 b/scripts/run-integration-tests.ps1 index d9e2ef7..9cf8438 100644 --- a/scripts/run-integration-tests.ps1 +++ b/scripts/run-integration-tests.ps1 @@ -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