From f2ad5ecfa3f5a5ea2b4e9b28f481546d0f265447 Mon Sep 17 00:00:00 2001 From: IK Date: Thu, 10 Sep 2026 18:08:31 +0700 Subject: [PATCH] A01: Per-source health affects community_scheduler overall status Bug: community_scheduler always had status='ready' even when individual sources were failed or stale. Success of one source masked failure of another. Fix: - Overall status is 'degraded' if any enabled source has failed - Overall status is 'stale' if all sources are stale but none failed - Overall status is 'ready' only when at least one source is healthy - Overall status is 'not_started' when no sources are enabled - Readiness (ready flag) still NOT blocked by import health (A01 requirement) Verification: - 7/7 readiness tests pass - 121/121 Python tests pass (1 skipped) - Failed/stale sources are now visible in JSON without blocking scheduler --- apps/api/app/readiness.py | 16 +++++++++++++++- apps/api/tests/test_readiness.py | 8 ++++---- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/apps/api/app/readiness.py b/apps/api/app/readiness.py index 2f386c5..7a17e70 100644 --- a/apps/api/app/readiness.py +++ b/apps/api/app/readiness.py @@ -72,11 +72,14 @@ def readiness_report( # Community scheduler health — diagnostic only, never blocks readiness (A01) # Track per-source health with rotation, backoff, last success, and stalled attempts + # Overall status reflects worst-case source health (success of one does not mask failure of another) try: enabled_sources = list(session.scalars( select(DataSource).where(DataSource.enabled.is_(True)).order_by(DataSource.key) )) source_health: dict[str, dict[str, object]] = {} + has_any_failure = False + has_any_success = False for source in enabled_sources: latest_run = session.scalar( select(CommunityImportRun) @@ -108,7 +111,18 @@ def readiness_report( "backoff_recommended": recent_failures >= 5, "blocking": False, } - components["community_scheduler"] = {"status": "ready", "sources": source_health} + if healthy: + has_any_success = True + elif latest_run.status == "failed": + has_any_failure = True + # Overall status: "degraded" if any source failed, "ready" if all healthy, "stale" if no failures but stale + if has_any_failure: + scheduler_status = "degraded" + elif has_any_success: + scheduler_status = "ready" + else: + scheduler_status = "stale" if enabled_sources else "not_started" + components["community_scheduler"] = {"status": scheduler_status, "sources": source_health} except Exception: components["community_scheduler"] = {"status": "unknown", "sources": {}} diff --git a/apps/api/tests/test_readiness.py b/apps/api/tests/test_readiness.py index 52b8950..87363eb 100644 --- a/apps/api/tests/test_readiness.py +++ b/apps/api/tests/test_readiness.py @@ -122,8 +122,8 @@ def test_community_scheduler_stale_does_not_block_readiness() -> None: session, AvailableStorage(), import_required=False, import_interval_seconds=3600, community_import_interval_seconds=1800, now=now, ) - assert ready is True # A01: stale does NOT block - assert components["community_scheduler"]["status"] == "ready" + assert ready is True # A01: stale does NOT block readiness + assert components["community_scheduler"]["status"] == "stale" # Overall reflects stale source assert components["community_scheduler"]["sources"]["rf4db"]["status"] == "stale" assert components["community_scheduler"]["sources"]["rf4db"]["blocking"] is False @@ -148,8 +148,8 @@ def test_community_scheduler_failed_does_not_block_readiness() -> None: session, AvailableStorage(), import_required=False, import_interval_seconds=3600, community_import_interval_seconds=1800, now=now, ) - assert ready is True # A01: failed does NOT block - assert components["community_scheduler"]["status"] == "ready" + assert ready is True # A01: failed does NOT block readiness + assert components["community_scheduler"]["status"] == "degraded" # Overall reflects failed source assert components["community_scheduler"]["sources"]["rf4db"]["status"] == "failed" assert components["community_scheduler"]["sources"]["rf4db"]["blocking"] is False