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_live_data_source,
|
||||||
get_rcon_historical_read_model,
|
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_snapshot_storage import get_historical_snapshot
|
||||||
from .historical_snapshots import (
|
from .historical_snapshots import (
|
||||||
DEFAULT_MONTHLY_SNAPSHOT_WINDOW,
|
DEFAULT_MONTHLY_SNAPSHOT_WINDOW,
|
||||||
@@ -1097,6 +1093,22 @@ def build_elo_mmr_leaderboard_payload(
|
|||||||
server_id: str | None = None,
|
server_id: str | None = None,
|
||||||
) -> dict[str, object]:
|
) -> dict[str, object]:
|
||||||
"""Return the current Elo/MMR monthly leaderboard."""
|
"""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)
|
payload = list_elo_mmr_leaderboard_payload(server_id=server_id, limit=limit)
|
||||||
is_all_servers = server_id == ALL_SERVERS_SLUG
|
is_all_servers = server_id == ALL_SERVERS_SLUG
|
||||||
accuracy_contract = _build_elo_accuracy_contract(payload.get("capabilities_summary"))
|
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,
|
server_id: str | None = None,
|
||||||
) -> dict[str, object]:
|
) -> dict[str, object]:
|
||||||
"""Return one Elo/MMR player profile."""
|
"""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)
|
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")
|
source_policy = list_elo_mmr_leaderboard_payload(server_id=server_id, limit=1).get("source_policy")
|
||||||
accuracy_contract = _build_elo_player_accuracy_contract(profile)
|
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]:
|
def _build_elo_player_accuracy_contract(profile: dict[str, object] | None) -> dict[str, object]:
|
||||||
if not isinstance(profile, dict):
|
if not isinstance(profile, dict):
|
||||||
return _build_elo_accuracy_contract(None)
|
return _build_elo_accuracy_contract(None)
|
||||||
|
|||||||
@@ -42,6 +42,20 @@ if (-not (Test-Path "ai/reports/.gitkeep")) {
|
|||||||
throw "Missing 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 "No product integration tests are configured for this platform-only scope."
|
||||||
|
Write-Host "Backend startup import check passed."
|
||||||
Write-Host "Platform validation passed."
|
Write-Host "Platform validation passed."
|
||||||
exit 0
|
exit 0
|
||||||
|
|||||||
Reference in New Issue
Block a user