Restore DEB training settlement worker

This commit is contained in:
2569718930@qq.com
2026-06-23 18:16:46 +08:00
parent 0c76874418
commit ba2db42eaa
10 changed files with 700 additions and 2 deletions
+21
View File
@@ -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
+77
View File
@@ -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"
+86 -1
View File
@@ -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)