BAI-3: harden live readiness diagnostics

Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
MS-01-Codex
2026-08-07 12:29:01 +08:00
co-authored by multica-agent
parent a326df2a8d
commit 777af0cb8b
8 changed files with 308 additions and 28 deletions
+2
View File
@@ -43,6 +43,7 @@ from backend.features.screener.service import (
from backend.features.sentiment import SentimentServiceMixin from backend.features.sentiment import SentimentServiceMixin
from backend.features.sentiment.routes import SentimentRoutesMixin from backend.features.sentiment.routes import SentimentRoutesMixin
from backend.features.system import SystemHttpMixin from backend.features.system import SystemHttpMixin
from backend.features.system.health import HealthServiceMixin
from backend.features.system.routes import SystemRoutesMixin from backend.features.system.routes import SystemRoutesMixin
from backend.features.system.service import SystemServiceMixin from backend.features.system.service import SystemServiceMixin
from backend.features.themes import ThemeServiceMixin from backend.features.themes import ThemeServiceMixin
@@ -77,6 +78,7 @@ LEGACY_SECRET_KEYS = {
class DashboardService( class DashboardService(
HealthServiceMixin,
SystemServiceMixin, SystemServiceMixin,
AccountApplicationMixin, AccountApplicationMixin,
JobServiceMixin, JobServiceMixin,
+94
View File
@@ -0,0 +1,94 @@
from __future__ import annotations
from datetime import datetime
from typing import Any
class HealthServiceMixin:
def health_status(self) -> dict[str, Any]:
"""Return a cheap public readiness summary without provider secrets."""
database_ready = False
database_status: dict[str, Any] = {}
try:
with self.database.connect() as connection:
database_ready = connection.execute("SELECT 1").fetchone() is not None
database_status = self.database.status()
except Exception:
database_ready = False
try:
recent_jobs = self.jobs.repository.recent(12)
except Exception:
recent_jobs = []
last_sync = database_status.get("last_sync") or {}
snapshot_dates = int(database_status.get("snapshot_dates") or 0)
data_configured = bool(self.configured)
last_sync_success = last_sync.get("status") == "success"
if data_configured and snapshot_dates and last_sync_success:
data_status = "ready"
elif snapshot_dates:
data_status = "degraded"
else:
data_status = "unavailable"
latest_job = recent_jobs[0] if recent_jobs else {}
failed_jobs = sum(1 for job in recent_jobs if job.get("status") == "failed")
if not database_ready:
jobs_status = "unavailable"
elif latest_job.get("status") == "running":
jobs_status = "running"
elif latest_job.get("status") == "failed":
jobs_status = "degraded"
else:
jobs_status = "ready"
platform = self._platform_llm_profile()
primary_ready = self._profile_configured(platform["primary"])
fallback_ready = self._profile_configured(platform["fallback"])
models_status = "ready" if primary_ready else "unavailable"
component_states = (
"ready",
"ready" if database_ready else "unavailable",
data_status,
jobs_status,
models_status,
)
return {
"ok": database_ready,
"status": (
"ready"
if all(state in {"ready", "running"} for state in component_states)
else "degraded"
),
"account_required": True,
"time": datetime.now().astimezone().isoformat(timespec="seconds"),
"components": {
"process": {"status": "ready"},
"database": {
"status": "ready" if database_ready else "unavailable",
"snapshot_dates": snapshot_dates,
"last_snapshot_at": database_status.get("updated_at") or "",
},
"data_sources": {
"status": data_status,
"configured": data_configured,
"snapshot_available": snapshot_dates > 0,
"latest_trade_date": str(last_sync.get("trade_date") or ""),
"last_success_at": (
str(last_sync.get("finished_at") or "") if last_sync_success else ""
),
},
"jobs": {
"status": jobs_status,
"recent_failures": failed_jobs,
"latest_finished_at": str(latest_job.get("finished_at") or ""),
},
"models": {
"status": models_status,
"primary": "ready" if primary_ready else "unavailable",
"fallback": "ready" if fallback_ready else "disabled",
},
},
}
+1 -9
View File
@@ -1,20 +1,12 @@
from __future__ import annotations from __future__ import annotations
from datetime import datetime
from http import HTTPStatus from http import HTTPStatus
class SystemRoutesMixin: class SystemRoutesMixin:
def _handle_system_public_get(self, parsed) -> bool: def _handle_system_public_get(self, parsed) -> bool:
if parsed.path == "/api/health": if parsed.path == "/api/health":
self.send_json( self.send_json(self.application_service.health_status())
{
"ok": True,
"storage": "sqlite",
"account_required": True,
"time": datetime.now().astimezone().isoformat(timespec="seconds"),
}
)
return True return True
return False return False
+31 -3
View File
@@ -2,7 +2,7 @@ from __future__ import annotations
import json import json
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime from datetime import datetime, timedelta
from typing import Any from typing import Any
from database import ReviewDatabase from database import ReviewDatabase
@@ -27,9 +27,37 @@ class SQLiteJobRunRepository:
def start( def start(
self, job_id: str, idempotency_key: str, output_version: str, self, job_id: str, idempotency_key: str, output_version: str,
metadata: dict[str, Any] | None = None, metadata: dict[str, Any] | None = None,
) -> int: stale_after_seconds: int = 0,
now = datetime.now().astimezone().isoformat(timespec="seconds") ) -> int | None:
current = datetime.now().astimezone()
now = current.isoformat(timespec="seconds")
stale_before = (
current - timedelta(seconds=max(1, int(stale_after_seconds or 1)))
).isoformat(timespec="seconds")
with self.database.connect() as connection: with self.database.connect() as connection:
# The process-local runner lock cannot protect two server processes.
connection.execute("BEGIN IMMEDIATE")
connection.execute(
"""
UPDATE job_runs
SET status = 'failed', finished_at = ?, error_code = 'StaleRun',
message = 'Previous run exceeded its execution window.'
WHERE job_id = ? AND idempotency_key = ? AND status = 'running'
AND started_at < ?
""",
(now, job_id, idempotency_key, stale_before),
)
claimed = connection.execute(
"""
SELECT 1 FROM job_runs
WHERE job_id = ? AND idempotency_key = ?
AND status IN ('running', 'success')
LIMIT 1
""",
(job_id, idempotency_key),
).fetchone()
if claimed:
return None
row = connection.execute( row = connection.execute(
""" """
SELECT COALESCE(MAX(attempt), 0) + 1 AS attempt FROM job_runs SELECT COALESCE(MAX(attempt), 0) + 1 AS attempt FROM job_runs
+12 -6
View File
@@ -51,8 +51,7 @@ class InProcessJobRunner:
lock = self._lock(definition.lock_key) lock = self._lock(definition.lock_key)
if not lock.acquire(blocking=False): if not lock.acquire(blocking=False):
return False return False
self._execute_locked(job_id, idempotency_key, action, metadata, lock) return self._execute_locked(job_id, idempotency_key, action, metadata, lock)
return True
def start_scheduler( def start_scheduler(
self, callback: Callable[[], None], interval_seconds: float, self, callback: Callable[[], None], interval_seconds: float,
@@ -105,13 +104,19 @@ class InProcessJobRunner:
def _execute_locked( def _execute_locked(
self, job_id: str, idempotency_key: str, action: JobAction, self, job_id: str, idempotency_key: str, action: JobAction,
metadata: dict[str, Any] | None, lock: threading.Lock, metadata: dict[str, Any] | None, lock: threading.Lock,
) -> None: ) -> bool:
definition = self.registry.get(job_id) definition = self.registry.get(job_id)
try: try:
for attempt in range(1, definition.max_attempts + 1): for attempt in range(1, definition.max_attempts + 1):
run_id = self.repository.start( run_id = self.repository.start(
job_id, idempotency_key, definition.output_version, metadata job_id,
idempotency_key,
definition.output_version,
metadata,
definition.timeout_seconds,
) )
if run_id is None:
return False
started = time.perf_counter() started = time.perf_counter()
try: try:
result = action() result = action()
@@ -119,7 +124,7 @@ class InProcessJobRunner:
raise RuntimeError(str(result.get("error") or "Job reported failure")) raise RuntimeError(str(result.get("error") or "Job reported failure"))
elapsed_ms = round((time.perf_counter() - started) * 1000) elapsed_ms = round((time.perf_counter() - started) * 1000)
self.repository.finish(run_id, "success", elapsed_ms) self.repository.finish(run_id, "success", elapsed_ms)
return return True
except Exception as exc: except Exception as exc:
elapsed_ms = round((time.perf_counter() - started) * 1000) elapsed_ms = round((time.perf_counter() - started) * 1000)
self.repository.finish( self.repository.finish(
@@ -127,7 +132,8 @@ class InProcessJobRunner:
type(exc).__name__, str(exc), type(exc).__name__, str(exc),
) )
if attempt >= definition.max_attempts: if attempt >= definition.max_attempts:
return return True
return True
finally: finally:
lock.release() lock.release()
+10 -10
View File
@@ -609,16 +609,16 @@
"bytes": 6983, "bytes": 6983,
"lines": 146 "lines": 146
}, },
{
"path": "backend/application.py",
"bytes": 6837,
"lines": 180
},
{ {
"path": "backend/data/providers/tushare_daily.py", "path": "backend/data/providers/tushare_daily.py",
"bytes": 6837, "bytes": 6837,
"lines": 160 "lines": 160
}, },
{
"path": "backend/application.py",
"bytes": 6751,
"lines": 178
},
{ {
"path": "backend/features/market/insights_popularity.py", "path": "backend/features/market/insights_popularity.py",
"bytes": 6739, "bytes": 6739,
@@ -829,11 +829,6 @@
"bytes": 1455, "bytes": 1455,
"lines": 48 "lines": 48
}, },
{
"path": "backend/features/system/routes.py",
"bytes": 1423,
"lines": 40
},
{ {
"path": "backend/features/themes/routes.py", "path": "backend/features/themes/routes.py",
"bytes": 1337, "bytes": 1337,
@@ -844,6 +839,11 @@
"bytes": 1195, "bytes": 1195,
"lines": 30 "lines": 30
}, },
{
"path": "backend/features/system/routes.py",
"bytes": 1178,
"lines": 32
},
{ {
"path": "frontend/pages/ladder/page.html", "path": "frontend/pages/ladder/page.html",
"bytes": 1143, "bytes": 1143,
+22
View File
@@ -59,6 +59,28 @@ class JobRunnerTests(unittest.TestCase):
release.set() release.set()
self.assertTrue(self.runner.wait_for_idle()) self.assertTrue(self.runner.wait_for_idle())
def test_database_claim_rejects_the_same_job_from_another_runner(self) -> None:
other_runner = InProcessJobRunner(JobRegistry.load(), self.repository)
entered = threading.Event()
release = threading.Event()
calls = []
def wait() -> None:
calls.append("first")
entered.set()
release.wait(2)
self.assertTrue(self.runner.submit("screener.automatic", "shared-key", wait))
self.assertTrue(entered.wait(1))
self.assertFalse(
other_runner.run_inline(
"screener.automatic", "shared-key", lambda: calls.append("second")
)
)
release.set()
self.assertTrue(self.runner.wait_for_idle())
self.assertEqual(calls, ["first"])
def test_failed_status_payload_is_recorded_as_failure(self) -> None: def test_failed_status_payload_is_recorded_as_failure(self) -> None:
self.assertTrue( self.assertTrue(
self.runner.run_inline( self.runner.run_inline(
+136
View File
@@ -0,0 +1,136 @@
from __future__ import annotations
import json
import unittest
from types import SimpleNamespace
from backend.features.system.health import HealthServiceMixin
from backend.features.system.routes import SystemRoutesMixin
class _Connection:
def __enter__(self):
return self
def __exit__(self, *_args):
return None
def execute(self, _query: str):
return self
def fetchone(self):
return (1,)
class _Database:
def connect(self):
return _Connection()
def status(self):
return {
"database": "secret-review.db",
"snapshot_dates": 31,
"updated_at": "2026-08-07T11:34:55+08:00",
"last_sync": {
"id": 991,
"trade_date": "20260807",
"source": "tushare",
"status": "success",
"finished_at": "2026-08-07T11:34:55+08:00",
"message": "https://provider.invalid?token=secret",
},
}
class _Service(HealthServiceMixin):
def __init__(self):
self.database = _Database()
self.jobs = SimpleNamespace(
repository=SimpleNamespace(
recent=lambda _limit: [
{
"id": 77,
"status": "success",
"finished_at": "2026-08-07T11:34:55+08:00",
"message": "secret upstream response",
}
]
)
)
self._system_credentials = {
"tushare_token": "secret-token",
"primary_model_id": "primary-id",
"fallback_model_id": "fallback-id",
}
@property
def configured(self):
return bool(self._system_credentials.get("tushare_token"))
def _platform_llm_profile(self):
return {
"primary": {
"api_key": "secret-primary-key",
"base_url": "https://models.invalid/v1",
"model": "secret-primary-model",
},
"fallback": {
"api_key": "secret-fallback-key",
"base_url": "https://models.invalid/v1",
"model": "secret-fallback-model",
},
}
@staticmethod
def _profile_configured(profile):
return all(profile.get(key) for key in ("api_key", "base_url", "model"))
class SystemHealthTests(unittest.TestCase):
def test_public_route_uses_the_service_health_contract(self):
handler = SystemRoutesMixin()
expected = {"ok": True, "components": {"process": {"status": "ready"}}}
handler.application_service = SimpleNamespace(health_status=lambda: expected)
responses = []
handler.send_json = responses.append
handled = handler._handle_system_public_get(SimpleNamespace(path="/api/health"))
self.assertTrue(handled)
self.assertEqual(responses, [expected])
def test_health_distinguishes_runtime_components(self):
result = _Service().health_status()
self.assertTrue(result["ok"])
self.assertEqual(result["status"], "ready")
self.assertEqual(
set(result["components"]),
{"process", "database", "data_sources", "jobs", "models"},
)
self.assertEqual(result["components"]["data_sources"]["latest_trade_date"], "20260807")
self.assertEqual(result["components"]["models"]["primary"], "ready")
self.assertEqual(result["components"]["models"]["fallback"], "ready")
def test_public_health_never_exposes_engineering_or_secret_fields(self):
payload = json.dumps(_Service().health_status(), ensure_ascii=False).lower()
for forbidden in (
"tushare",
"ifind",
"secret",
"token",
"https://",
".db",
"primary-id",
"fallback-id",
"model",
):
if forbidden == "model":
self.assertNotIn("secret-primary-model", payload)
else:
self.assertNotIn(forbidden, payload)
if __name__ == "__main__":
unittest.main()