fix(awooop): fallback AI alert card readback to direct DB
Some checks failed
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 56s
CD Pipeline / build-and-deploy (push) Successful in 4m55s
CD Pipeline / post-deploy-checks (push) Has been cancelled
Some checks failed
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 56s
CD Pipeline / build-and-deploy (push) Successful in 4m55s
CD Pipeline / post-deploy-checks (push) Has been cancelled
This commit is contained in:
@@ -111,6 +111,18 @@ _AI_ALERT_CARD_QUERY_TIMEOUT_SECONDS = float(
|
||||
_AI_ALERT_CARD_STATEMENT_TIMEOUT_MS = int(
|
||||
os.getenv("AWOOOP_AI_ALERT_CARD_STATEMENT_TIMEOUT_MS", "200")
|
||||
)
|
||||
_AI_ALERT_CARD_DIRECT_CONNECT_TIMEOUT_SECONDS = float(
|
||||
os.getenv("AWOOOP_AI_ALERT_CARD_DIRECT_CONNECT_TIMEOUT_SECONDS", "1.5")
|
||||
)
|
||||
_AI_ALERT_CARD_DIRECT_QUERY_TIMEOUT_SECONDS = float(
|
||||
os.getenv("AWOOOP_AI_ALERT_CARD_DIRECT_QUERY_TIMEOUT_SECONDS", "1.25")
|
||||
)
|
||||
_AI_ALERT_CARD_DIRECT_CLOSE_TIMEOUT_SECONDS = float(
|
||||
os.getenv("AWOOOP_AI_ALERT_CARD_DIRECT_CLOSE_TIMEOUT_SECONDS", "0.25")
|
||||
)
|
||||
_AI_ALERT_CARD_DIRECT_STATEMENT_TIMEOUT_MS = int(
|
||||
os.getenv("AWOOOP_AI_ALERT_CARD_DIRECT_STATEMENT_TIMEOUT_MS", "1000")
|
||||
)
|
||||
_AI_ALERT_CARD_LEARNING_REGISTRY_TARGETS: tuple[dict[str, str], ...] = (
|
||||
{
|
||||
"target": "km",
|
||||
@@ -1406,6 +1418,7 @@ async def list_ai_alert_card_delivery_readback(
|
||||
params["lane"] = normalized_lane
|
||||
|
||||
where_sql = " AND ".join(where_clauses)
|
||||
direct_fallback_used = False
|
||||
summary_sql = text(f"""
|
||||
SELECT
|
||||
COUNT(*) AS total,
|
||||
@@ -1527,27 +1540,38 @@ async def list_ai_alert_card_delivery_readback(
|
||||
per_page=normalized_per_page,
|
||||
error_type=type(exc).__name__,
|
||||
)
|
||||
cached_fallback = await _get_cached_ai_alert_card_delivery_readback(
|
||||
cache_key=cache_key,
|
||||
direct_readback = await _load_ai_alert_card_delivery_readback_direct(
|
||||
project_id=normalized_project_id,
|
||||
event_type=normalized_event_type,
|
||||
lane=normalized_lane,
|
||||
page=normalized_page,
|
||||
per_page=normalized_per_page,
|
||||
)
|
||||
if cached_fallback is not None and "learning_registry" in cached_fallback:
|
||||
return _ai_alert_card_delivery_cached_live_timeout_response(
|
||||
cached_response=cached_fallback,
|
||||
if direct_readback is not None:
|
||||
summary_row, rows = direct_readback
|
||||
direct_fallback_used = True
|
||||
else:
|
||||
cached_fallback = await _get_cached_ai_alert_card_delivery_readback(
|
||||
cache_key=cache_key,
|
||||
project_id=normalized_project_id,
|
||||
event_type=normalized_event_type,
|
||||
lane=normalized_lane,
|
||||
page=normalized_page,
|
||||
per_page=normalized_per_page,
|
||||
)
|
||||
if cached_fallback is not None and "learning_registry" in cached_fallback:
|
||||
return _ai_alert_card_delivery_cached_live_timeout_response(
|
||||
cached_response=cached_fallback,
|
||||
error_type=type(exc).__name__,
|
||||
)
|
||||
return _ai_alert_card_delivery_source_unavailable_response(
|
||||
project_id=normalized_project_id,
|
||||
event_type=normalized_event_type or None,
|
||||
lane=normalized_lane or None,
|
||||
page=normalized_page,
|
||||
per_page=normalized_per_page,
|
||||
error_type=type(exc).__name__,
|
||||
)
|
||||
return _ai_alert_card_delivery_source_unavailable_response(
|
||||
project_id=normalized_project_id,
|
||||
event_type=normalized_event_type or None,
|
||||
lane=normalized_lane or None,
|
||||
page=normalized_page,
|
||||
per_page=normalized_per_page,
|
||||
error_type=type(exc).__name__,
|
||||
)
|
||||
|
||||
summary = _ai_alert_card_delivery_summary_from_row(
|
||||
summary_row,
|
||||
@@ -1555,6 +1579,14 @@ async def list_ai_alert_card_delivery_readback(
|
||||
event_type=normalized_event_type or None,
|
||||
lane=normalized_lane or None,
|
||||
)
|
||||
if direct_fallback_used:
|
||||
summary.update({
|
||||
"db_read_status": "direct_connection_after_session_timeout",
|
||||
"live_refresh_status": "live_refresh_served_by_direct_connection",
|
||||
"source_unavailable_reason": "session_timeout_recovered",
|
||||
"source_unavailable_next_action": "keep_direct_fallback_and_retry_session_pool",
|
||||
"controlled_repair_queue_present": False,
|
||||
})
|
||||
items = [_ai_alert_card_delivery_item(row) for row in rows]
|
||||
learning_registry = _ai_alert_card_learning_registry_readback(
|
||||
items=items,
|
||||
@@ -1608,6 +1640,190 @@ async def list_ai_alert_card_delivery_readback(
|
||||
return response
|
||||
|
||||
|
||||
async def _load_ai_alert_card_delivery_readback_direct(
|
||||
*,
|
||||
project_id: str,
|
||||
event_type: str,
|
||||
lane: str,
|
||||
page: int,
|
||||
per_page: int,
|
||||
) -> tuple[dict[str, Any], list[dict[str, Any]]] | None:
|
||||
"""Read AI alert card rows through a short-lived DB connection under pool pressure."""
|
||||
|
||||
db_url = get_settings().DATABASE_URL.replace(
|
||||
"postgresql+asyncpg://",
|
||||
"postgresql://",
|
||||
)
|
||||
args: list[Any] = [project_id]
|
||||
where_clauses = [
|
||||
"m.project_id = $1",
|
||||
"m.channel_type = 'telegram'",
|
||||
"COALESCE(m.source_envelope::jsonb, '{}'::jsonb) ? 'ai_automation_alert_card'",
|
||||
]
|
||||
if event_type:
|
||||
args.append(event_type)
|
||||
where_clauses.append(
|
||||
"COALESCE(m.source_envelope::jsonb, '{}'::jsonb) #>> "
|
||||
f"'{{ai_automation_alert_card,event_type}}' = ${len(args)}"
|
||||
)
|
||||
if lane:
|
||||
args.append(lane)
|
||||
where_clauses.append(
|
||||
"COALESCE(m.source_envelope::jsonb, '{}'::jsonb) #>> "
|
||||
f"'{{ai_automation_alert_card,lane}}' = ${len(args)}"
|
||||
)
|
||||
where_sql = " AND ".join(where_clauses)
|
||||
limit_param = len(args) + 1
|
||||
offset_param = len(args) + 2
|
||||
offset = (page - 1) * per_page
|
||||
try:
|
||||
import asyncpg
|
||||
|
||||
conn = await asyncio.wait_for(
|
||||
asyncpg.connect(db_url),
|
||||
timeout=_AI_ALERT_CARD_DIRECT_CONNECT_TIMEOUT_SECONDS,
|
||||
)
|
||||
try:
|
||||
await asyncio.wait_for(
|
||||
conn.execute("SELECT set_config('app.project_id', $1, TRUE)", project_id),
|
||||
timeout=_AI_ALERT_CARD_DIRECT_QUERY_TIMEOUT_SECONDS,
|
||||
)
|
||||
await asyncio.wait_for(
|
||||
conn.execute(
|
||||
"SET statement_timeout = "
|
||||
f"'{int(_AI_ALERT_CARD_DIRECT_STATEMENT_TIMEOUT_MS)}ms'"
|
||||
),
|
||||
timeout=_AI_ALERT_CARD_DIRECT_QUERY_TIMEOUT_SECONDS,
|
||||
)
|
||||
summary_row = await asyncio.wait_for(
|
||||
conn.fetchrow(f"""
|
||||
SELECT
|
||||
COUNT(*) AS total,
|
||||
COUNT(*) FILTER (WHERE m.send_status = 'sent') AS sent_total,
|
||||
COUNT(*) FILTER (WHERE m.send_status = 'failed') AS failed_total,
|
||||
COUNT(*) FILTER (WHERE m.send_status = 'pending') AS pending_total,
|
||||
COUNT(*) FILTER (WHERE m.send_status = 'shadow') AS shadow_total,
|
||||
COUNT(*) FILTER (
|
||||
WHERE COALESCE(
|
||||
COALESCE(m.source_envelope::jsonb, '{{}}'::jsonb) #>>
|
||||
'{{ai_automation_alert_card,delivery_receipt_readback_required}}',
|
||||
''
|
||||
) = 'true'
|
||||
) AS delivery_receipt_required_total,
|
||||
COUNT(*) FILTER (
|
||||
WHERE COALESCE(
|
||||
COALESCE(m.source_envelope::jsonb, '{{}}'::jsonb) #>>
|
||||
'{{ai_automation_alert_card,runtime_write_gate_count}}',
|
||||
'0'
|
||||
) <> '0'
|
||||
) AS runtime_write_gate_open_count,
|
||||
COUNT(*) FILTER (
|
||||
WHERE COALESCE(
|
||||
COALESCE(m.source_envelope::jsonb, '{{}}'::jsonb) #>>
|
||||
'{{ai_automation_alert_card,delivery_receipt_readback_required}}',
|
||||
''
|
||||
) = 'true'
|
||||
AND COALESCE(
|
||||
COALESCE(m.source_envelope::jsonb, '{{}}'::jsonb) #>>
|
||||
'{{ai_automation_alert_card,event_type}}',
|
||||
''
|
||||
) <> ''
|
||||
AND COALESCE(
|
||||
COALESCE(m.source_envelope::jsonb, '{{}}'::jsonb) #>>
|
||||
'{{ai_automation_alert_card,lane}}',
|
||||
''
|
||||
) <> ''
|
||||
) AS learning_writeback_ready_total,
|
||||
MAX(m.sent_at) AS latest_sent_at,
|
||||
MAX(m.queued_at) AS latest_queued_at
|
||||
FROM awooop_outbound_message m
|
||||
WHERE {where_sql}
|
||||
""", *args),
|
||||
timeout=_AI_ALERT_CARD_DIRECT_QUERY_TIMEOUT_SECONDS,
|
||||
)
|
||||
list_rows = await asyncio.wait_for(
|
||||
conn.fetch(f"""
|
||||
SELECT
|
||||
m.message_id,
|
||||
m.project_id,
|
||||
m.run_id,
|
||||
m.channel_type,
|
||||
m.message_type,
|
||||
m.provider_message_id,
|
||||
m.send_status,
|
||||
m.send_error,
|
||||
m.queued_at,
|
||||
m.sent_at,
|
||||
m.triggered_by_state,
|
||||
(
|
||||
COALESCE(m.source_envelope::jsonb, '{{}}'::jsonb)
|
||||
-> 'ai_automation_alert_card'
|
||||
)::text AS alert_card_json,
|
||||
(
|
||||
COALESCE(m.source_envelope::jsonb, '{{}}'::jsonb)
|
||||
-> 'source_refs'
|
||||
)::text AS source_refs_json,
|
||||
r.agent_id,
|
||||
r.state AS run_state,
|
||||
r.created_at AS run_created_at
|
||||
FROM awooop_outbound_message m
|
||||
LEFT JOIN awooop_run_state r
|
||||
ON r.project_id = m.project_id
|
||||
AND r.run_id = m.run_id
|
||||
WHERE {where_sql}
|
||||
ORDER BY COALESCE(m.sent_at, m.queued_at) DESC, m.message_id DESC
|
||||
LIMIT ${limit_param} OFFSET ${offset_param}
|
||||
""", *args, per_page, offset),
|
||||
timeout=_AI_ALERT_CARD_DIRECT_QUERY_TIMEOUT_SECONDS,
|
||||
)
|
||||
return (
|
||||
dict(summary_row) if summary_row is not None else {},
|
||||
[_ai_alert_card_direct_row(row) for row in list_rows],
|
||||
)
|
||||
finally:
|
||||
try:
|
||||
await asyncio.wait_for(
|
||||
conn.close(),
|
||||
timeout=_AI_ALERT_CARD_DIRECT_CLOSE_TIMEOUT_SECONDS,
|
||||
)
|
||||
except Exception as close_exc: # pragma: no cover - live cleanup
|
||||
logger.warning(
|
||||
"operator_ai_alert_card_delivery_direct_close_unavailable",
|
||||
project_id=project_id,
|
||||
error_type=type(close_exc).__name__,
|
||||
)
|
||||
except Exception as exc: # pragma: no cover - live DB pressure
|
||||
logger.warning(
|
||||
"operator_ai_alert_card_delivery_direct_readback_unavailable",
|
||||
project_id=project_id,
|
||||
event_type=event_type,
|
||||
lane=lane,
|
||||
page=page,
|
||||
per_page=per_page,
|
||||
error_type=type(exc).__name__,
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def _ai_alert_card_direct_row(row: Mapping[str, Any]) -> dict[str, Any]:
|
||||
payload = dict(row)
|
||||
payload["alert_card"] = _json_object(payload.pop("alert_card_json", None))
|
||||
payload["source_refs"] = _json_object(payload.pop("source_refs_json", None))
|
||||
return payload
|
||||
|
||||
|
||||
def _json_object(value: Any) -> dict[str, Any]:
|
||||
if isinstance(value, Mapping):
|
||||
return dict(value)
|
||||
if isinstance(value, str) and value.strip():
|
||||
try:
|
||||
payload = json.loads(value)
|
||||
except json.JSONDecodeError:
|
||||
return {}
|
||||
return dict(payload) if isinstance(payload, Mapping) else {}
|
||||
return {}
|
||||
|
||||
|
||||
async def _get_cached_ai_alert_card_delivery_readback(
|
||||
*,
|
||||
cache_key: Mapping[str, Any],
|
||||
|
||||
Reference in New Issue
Block a user