From abd7ec08157d1583c8f1928519e43131663c2f68 Mon Sep 17 00:00:00 2001 From: ogt Date: Thu, 9 Jul 2026 22:47:29 +0800 Subject: [PATCH] fix(agents): bound AI alert card DB readback --- .../src/services/platform_operator_service.py | 51 +++++++++++++++++-- .../test_awooop_operator_timeline_labels.py | 45 ++++++++++++++++ 2 files changed, 93 insertions(+), 3 deletions(-) diff --git a/apps/api/src/services/platform_operator_service.py b/apps/api/src/services/platform_operator_service.py index bfefab1ed..94a90bf6c 100644 --- a/apps/api/src/services/platform_operator_service.py +++ b/apps/api/src/services/platform_operator_service.py @@ -99,6 +99,18 @@ _CALLBACK_REPLY_CACHE_TTL_SECONDS = int( _AI_ALERT_CARD_CACHE_TTL_SECONDS = int( os.getenv("AWOOOP_AI_ALERT_CARD_CACHE_TTL_SECONDS", "20") ) +_AI_ALERT_CARD_DB_CONTEXT_TIMEOUT_SECONDS = float( + os.getenv("AWOOOP_AI_ALERT_CARD_DB_CONTEXT_TIMEOUT_SECONDS", "0.25") +) +_AI_ALERT_CARD_DB_CONTEXT_EXIT_TIMEOUT_SECONDS = float( + os.getenv("AWOOOP_AI_ALERT_CARD_DB_CONTEXT_EXIT_TIMEOUT_SECONDS", "0.20") +) +_AI_ALERT_CARD_QUERY_TIMEOUT_SECONDS = float( + os.getenv("AWOOOP_AI_ALERT_CARD_QUERY_TIMEOUT_SECONDS", "0.25") +) +_AI_ALERT_CARD_STATEMENT_TIMEOUT_MS = int( + os.getenv("AWOOOP_AI_ALERT_CARD_STATEMENT_TIMEOUT_MS", "200") +) _AI_ALERT_CARD_LEARNING_REGISTRY_TARGETS: tuple[dict[str, str], ...] = ( { "target": "km", @@ -1476,11 +1488,44 @@ async def list_ai_alert_card_delivery_readback( """) try: - async with get_db_context(normalized_project_id) as db: - summary_result = await db.execute(summary_sql, params) + db_context = get_db_context(normalized_project_id) + db = await asyncio.wait_for( + db_context.__aenter__(), + timeout=_AI_ALERT_CARD_DB_CONTEXT_TIMEOUT_SECONDS, + ) + try: + await asyncio.wait_for( + db.execute( + text( + "SET LOCAL statement_timeout = " + f"'{int(_AI_ALERT_CARD_STATEMENT_TIMEOUT_MS)}ms'" + ), + {}, + ), + timeout=_AI_ALERT_CARD_QUERY_TIMEOUT_SECONDS, + ) + summary_result = await asyncio.wait_for( + db.execute(summary_sql, params), + timeout=_AI_ALERT_CARD_QUERY_TIMEOUT_SECONDS, + ) summary_row = summary_result.mappings().first() or {} - rows_result = await db.execute(list_sql, params) + rows_result = await asyncio.wait_for( + db.execute(list_sql, params), + timeout=_AI_ALERT_CARD_QUERY_TIMEOUT_SECONDS, + ) rows = list(rows_result.mappings().all()) + finally: + try: + await asyncio.wait_for( + db_context.__aexit__(None, None, None), + timeout=_AI_ALERT_CARD_DB_CONTEXT_EXIT_TIMEOUT_SECONDS, + ) + except Exception as exit_exc: # pragma: no cover - pool pressure + logger.warning( + "operator_ai_alert_card_delivery_readback_context_exit_unavailable", + project_id=normalized_project_id, + error_type=type(exit_exc).__name__, + ) except Exception as exc: # pragma: no cover - production DB drift/pressure logger.warning( "operator_ai_alert_card_delivery_readback_source_unavailable", diff --git a/apps/api/tests/test_awooop_operator_timeline_labels.py b/apps/api/tests/test_awooop_operator_timeline_labels.py index 7f4a8f407..dcc398951 100644 --- a/apps/api/tests/test_awooop_operator_timeline_labels.py +++ b/apps/api/tests/test_awooop_operator_timeline_labels.py @@ -1328,6 +1328,51 @@ async def test_list_ai_alert_card_delivery_readback_failsofts_db_unavailable( assert payload["learning_registry"]["missing_target_count"] == 6 +@pytest.mark.asyncio +async def test_list_ai_alert_card_delivery_readback_failsofts_slow_db( + monkeypatch: pytest.MonkeyPatch, +) -> None: + class SlowDb: + async def execute(self, *_args, **_kwargs): + await asyncio.sleep(1) + + class SlowDbContext: + async def __aenter__(self): + return SlowDb() + + async def __aexit__(self, exc_type, exc, tb): + return False + + monkeypatch.setattr( + platform_operator_service, + "_AI_ALERT_CARD_QUERY_TIMEOUT_SECONDS", + 0.01, + ) + monkeypatch.setattr( + platform_operator_service, + "_AI_ALERT_CARD_DB_CONTEXT_EXIT_TIMEOUT_SECONDS", + 0.01, + ) + monkeypatch.setattr( + platform_operator_service, + "get_db_context", + lambda _project_id: SlowDbContext(), + ) + + payload = await platform_operator_service.list_ai_alert_card_delivery_readback( + project_id="awoooi", + page=1, + per_page=6, + refresh=True, + ) + + assert payload["summary"]["db_read_status"] == "unavailable" + assert payload["summary"]["source_unavailable_reason"] == "TimeoutError" + assert payload["summary"]["learning_registry_next_action"] == ( + "repair_awooop_ai_alert_card_delivery_readback_then_consume_learning_registry" + ) + + def test_list_callback_replies_keeps_audit_summary_separate_from_km_summary() -> None: source = inspect.getsource(platform_operator_service.list_callback_replies)