Restore DEB training settlement worker
This commit is contained in:
@@ -94,6 +94,13 @@ def test_docker_compose_isolates_collector_from_web_and_bot_services():
|
||||
1,
|
||||
)[0]
|
||||
warmer_block = compose.split(" polyweather_warmer:", 1)[1].split(
|
||||
"\n polyweather_training_settlement:",
|
||||
1,
|
||||
)[0]
|
||||
training_settlement_block = compose.split(
|
||||
" polyweather_training_settlement:",
|
||||
1,
|
||||
)[1].split(
|
||||
"\nx-polyweather-base:",
|
||||
1,
|
||||
)[0]
|
||||
@@ -102,6 +109,7 @@ def test_docker_compose_isolates_collector_from_web_and_bot_services():
|
||||
assert "POLYWEATHER_SERVICE_ROLE: bot" in compose
|
||||
assert "POLYWEATHER_SERVICE_ROLE: collector" in collector_block
|
||||
assert "POLYWEATHER_SERVICE_ROLE: warmer" in warmer_block
|
||||
assert "POLYWEATHER_SERVICE_ROLE: training_settlement" in training_settlement_block
|
||||
assert "redis-server --appendonly yes --maxmemory ${POLYWEATHER_REDIS_MAXMEMORY:-512mb} --maxmemory-policy noeviction" in compose
|
||||
assert "POLYWEATHER_SCAN_TERMINAL_PREWARM_ENABLED: 'false'" in bot_block
|
||||
assert "POLYWEATHER_EVENT_STORE: ${POLYWEATHER_EVENT_STORE:-redis}" in web_block
|
||||
@@ -117,6 +125,18 @@ def test_docker_compose_isolates_collector_from_web_and_bot_services():
|
||||
assert "POLYWEATHER_OBSERVATION_COLLECTOR_ENABLED: 'false'" in web_block
|
||||
assert "POLYWEATHER_OBSERVATION_COLLECTOR_ENABLED: 'true'" in collector_block
|
||||
assert "POLYWEATHER_OBSERVATION_COLLECTOR_ENABLED: 'false'" in warmer_block
|
||||
assert "POLYWEATHER_OBSERVATION_COLLECTOR_ENABLED: 'false'" in training_settlement_block
|
||||
assert "command: python -m web.training_settlement_worker" in training_settlement_block
|
||||
assert (
|
||||
"POLYWEATHER_TRAINING_SETTLEMENT_INTERVAL_SEC: "
|
||||
"${POLYWEATHER_TRAINING_SETTLEMENT_INTERVAL_SEC:-21600}"
|
||||
in training_settlement_block
|
||||
)
|
||||
assert (
|
||||
"POLYWEATHER_TRAINING_SETTLEMENT_LOOKBACK_DAYS: "
|
||||
"${POLYWEATHER_TRAINING_SETTLEMENT_LOOKBACK_DAYS:-10}"
|
||||
in training_settlement_block
|
||||
)
|
||||
assert "POLYWEATHER_CITY_DETAIL_BATCH_CONCURRENCY: ${POLYWEATHER_CITY_DETAIL_BATCH_CONCURRENCY:-3}" in web_block
|
||||
assert "POLYWEATHER_CITY_DETAIL_BATCH_GLOBAL_CONCURRENCY: ${POLYWEATHER_CITY_DETAIL_BATCH_GLOBAL_CONCURRENCY:-3}" in web_block
|
||||
assert "POLYWEATHER_CITY_DETAIL_BATCH_QUEUE_WAIT_MS: ${POLYWEATHER_CITY_DETAIL_BATCH_QUEUE_WAIT_MS:-3000}" in web_block
|
||||
@@ -287,6 +307,7 @@ def test_deploy_script_retries_compose_recreate_races():
|
||||
assert 'compose_up_retry "backend services" -d --no-deps polyweather_web polyweather' in script
|
||||
assert 'compose_up_retry "observation collector" -d --no-deps polyweather_collector' in script
|
||||
assert 'compose_up_retry "cache warmer" -d --no-deps polyweather_warmer' in script
|
||||
assert 'compose_up_retry "training settlement" -d --no-deps polyweather_training_settlement' in script
|
||||
assert 'compose_up_retry "frontend" -d --no-deps polyweather_frontend' in script
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
from web.training_settlement_service import run_training_settlement_cycle
|
||||
|
||||
|
||||
def test_training_settlement_cycle_runs_analysis_and_reconciles_supported_cities():
|
||||
calls = {"analysis": [], "reconcile": []}
|
||||
|
||||
def analysis_runner(city):
|
||||
calls["analysis"].append(city)
|
||||
return {"city": city, "deb": {"prediction": 31.2}}
|
||||
|
||||
def actual_reconciler(city, *, lookback_days):
|
||||
calls["reconcile"].append((city, lookback_days))
|
||||
return {"ok": True, "updated": 1}
|
||||
|
||||
result = run_training_settlement_cycle(
|
||||
city_registry={
|
||||
"shanghai": {"icao": "ZSSS", "settlement_source": "metar"},
|
||||
"legacy": {"settlement_source": "wunderground"},
|
||||
},
|
||||
analysis_runner=analysis_runner,
|
||||
actual_reconciler=actual_reconciler,
|
||||
lookback_days=9,
|
||||
)
|
||||
|
||||
assert result["ok"] is True
|
||||
assert result["processed"] == 1
|
||||
assert result["failed"] == 0
|
||||
assert result["unsupported"] == 1
|
||||
assert calls["analysis"] == ["shanghai"]
|
||||
assert calls["reconcile"] == [("shanghai", 9)]
|
||||
|
||||
|
||||
def test_training_settlement_cycle_continues_after_city_failure():
|
||||
calls = []
|
||||
|
||||
def analysis_runner(city):
|
||||
calls.append(city)
|
||||
if city == "shanghai":
|
||||
raise RuntimeError("analysis unavailable")
|
||||
return {"city": city}
|
||||
|
||||
result = run_training_settlement_cycle(
|
||||
city_registry={
|
||||
"shanghai": {"icao": "ZSSS", "settlement_source": "metar"},
|
||||
"tokyo": {"icao": "RJTT", "settlement_source": "metar"},
|
||||
},
|
||||
analysis_runner=analysis_runner,
|
||||
actual_reconciler=lambda city, *, lookback_days: {"ok": True, "updated": 0},
|
||||
lookback_days=3,
|
||||
)
|
||||
|
||||
assert result["ok"] is False
|
||||
assert result["processed"] == 1
|
||||
assert result["failed"] == 1
|
||||
assert calls == ["shanghai", "tokyo"]
|
||||
assert result["items"][0]["ok"] is False
|
||||
assert result["items"][1]["ok"] is True
|
||||
|
||||
|
||||
def test_training_settlement_cycle_skips_reconcile_for_non_reconcile_sources():
|
||||
calls = {"analysis": [], "reconcile": []}
|
||||
|
||||
result = run_training_settlement_cycle(
|
||||
city_registry={
|
||||
"taipei": {"icao": "RCSS", "settlement_source": "cwa"},
|
||||
},
|
||||
analysis_runner=lambda city: calls["analysis"].append(city) or {"city": city},
|
||||
actual_reconciler=lambda city, *, lookback_days: calls["reconcile"].append(city)
|
||||
or {"ok": True},
|
||||
lookback_days=3,
|
||||
)
|
||||
|
||||
assert result["ok"] is True
|
||||
assert result["processed"] == 1
|
||||
assert calls["analysis"] == ["taipei"]
|
||||
assert calls["reconcile"] == []
|
||||
assert result["items"][0]["reconcile"]["reason"] == "unsupported_reconcile_source"
|
||||
@@ -1,5 +1,6 @@
|
||||
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from types import SimpleNamespace
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
from starlette.requests import Request
|
||||
@@ -9,13 +10,16 @@ import web.services.auth_api as auth_api
|
||||
from web.app import app
|
||||
import web.routes as routes
|
||||
import web.services.ops_api as ops_api
|
||||
import web.diagnostics.health as diagnostics_health
|
||||
import web.scan_terminal_cache as scan_terminal_cache
|
||||
import web.scan_terminal_service as scan_terminal_service
|
||||
import web.services.system_api as system_api
|
||||
import web.services.city_api as city_api
|
||||
import web.services.city_runtime as city_runtime
|
||||
from web.services.observation_freshness import build_observation_freshness
|
||||
from web.scan_terminal_cache import scan_terminal_cache_key
|
||||
from src.database.runtime_state import TruthRecordRepository
|
||||
from src.database.runtime_state import RuntimeStateDB, TruthRecordRepository
|
||||
from src.utils.metrics import export_prometheus_metrics
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
@@ -83,6 +87,8 @@ def test_system_status_returns_summary_shape_for_ops_admin(monkeypatch):
|
||||
assert 'sse_connections' in payload['realtime']
|
||||
assert 'truth_records' in payload['training_data']
|
||||
assert 'training_features' in payload['training_data']
|
||||
assert 'stale_days' in payload['training_data']['truth_records']
|
||||
assert 'stale_days' in payload['training_data']['training_features']
|
||||
assert 'city_coverage' in payload['training_data']
|
||||
assert 'model_city_coverage' in payload['training_data']
|
||||
assert 'metar_entries' in payload['cache']
|
||||
@@ -121,6 +127,85 @@ def test_metrics_endpoint_returns_prometheus_payload_for_ops_admin(monkeypatch):
|
||||
assert 'polyweather_http_requests_total' in response.text
|
||||
|
||||
|
||||
def test_training_data_summary_reports_stale_days(tmp_path):
|
||||
db = RuntimeStateDB(str(tmp_path / "training.db"))
|
||||
yesterday = (datetime.now(timezone.utc).date() - timedelta(days=1)).strftime("%Y-%m-%d")
|
||||
stale_day = (datetime.now(timezone.utc).date() - timedelta(days=5)).strftime("%Y-%m-%d")
|
||||
with db.connect() as conn:
|
||||
conn.execute(
|
||||
"""
|
||||
INSERT INTO truth_records_store (
|
||||
city, target_date, actual_high, settlement_source, updated_at, is_final
|
||||
) VALUES ('shanghai', ?, 31.2, 'metar', 1, 1)
|
||||
""",
|
||||
(yesterday,),
|
||||
)
|
||||
conn.execute(
|
||||
"""
|
||||
INSERT INTO training_feature_records_store (
|
||||
city, target_date, updated_at, payload_json
|
||||
) VALUES ('shanghai', ?, 1, '{}')
|
||||
""",
|
||||
(stale_day,),
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
payload = diagnostics_health._training_data_summary(
|
||||
SimpleNamespace(db_path=db.db_path),
|
||||
{"shanghai": {"name": "Shanghai", "settlement_source": "metar", "icao": "ZSSS"}},
|
||||
)
|
||||
|
||||
assert payload["truth_records"]["stale_days"] == 1
|
||||
assert payload["training_features"]["stale_days"] == 5
|
||||
assert payload["stale"] is True
|
||||
|
||||
|
||||
def test_prometheus_exports_training_data_stale_metrics(monkeypatch, tmp_path):
|
||||
db = RuntimeStateDB(str(tmp_path / "training-metrics.db"))
|
||||
stale_day = (datetime.now(timezone.utc).date() - timedelta(days=4)).strftime("%Y-%m-%d")
|
||||
with db.connect() as conn:
|
||||
conn.execute(
|
||||
"""
|
||||
INSERT INTO daily_records_store (
|
||||
city, target_date, actual_high, deb_prediction, mu, updated_at, payload_json
|
||||
) VALUES ('shanghai', ?, 31.2, 31.0, 31.1, 1, '{}')
|
||||
""",
|
||||
(stale_day,),
|
||||
)
|
||||
conn.execute(
|
||||
"""
|
||||
INSERT INTO truth_records_store (
|
||||
city, target_date, actual_high, settlement_source, updated_at, is_final
|
||||
) VALUES ('shanghai', ?, 31.2, 'metar', 1, 1)
|
||||
""",
|
||||
(stale_day,),
|
||||
)
|
||||
conn.execute(
|
||||
"""
|
||||
INSERT INTO training_feature_records_store (
|
||||
city, target_date, updated_at, payload_json
|
||||
) VALUES ('shanghai', ?, 1, '{}')
|
||||
""",
|
||||
(stale_day,),
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
fake_db = SimpleNamespace(
|
||||
db_path=db.db_path,
|
||||
list_payment_audit_events=lambda **_kwargs: [],
|
||||
list_refund_cases=lambda **_kwargs: [],
|
||||
)
|
||||
monkeypatch.setattr(system_api, "DBManager", lambda: fake_db)
|
||||
|
||||
system_api._refresh_operational_metrics()
|
||||
metrics = export_prometheus_metrics()
|
||||
|
||||
assert "polyweather_daily_records_stale_days 4" in metrics
|
||||
assert "polyweather_truth_records_stale_days 4" in metrics
|
||||
assert "polyweather_training_features_stale_days 4" in metrics
|
||||
assert "polyweather_training_data_stale 1" in metrics
|
||||
|
||||
|
||||
def test_system_cache_status_requires_ops_admin(monkeypatch):
|
||||
monkeypatch.setattr(routes, "_assert_entitlement", lambda request: None)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user