加入告警去重與洞察向量回補
All checks were successful
CD Pipeline / deploy (push) Successful in 1m19s

This commit is contained in:
OoO
2026-04-29 23:10:27 +08:00
parent 0c2e9bbced
commit 78eebfbcfc
6 changed files with 155 additions and 0 deletions

View File

@@ -22,6 +22,9 @@ _QUEUE_PATH = os.getenv(
os.path.join(os.path.dirname(os.path.dirname(__file__)), "data", "event_router_failed_deliveries.jsonl"),
)
_QUEUE_LOCK = threading.Lock()
_DEDUP_LOCK = threading.Lock()
_EVENT_DEDUP: Dict[str, float] = {}
_DEFAULT_DEDUP_SEC = int(os.getenv("MOMO_EVENT_ROUTER_DEFAULT_DEDUP_SEC", "0"))
async def _handle_l1(event: Dict[str, Any], session_id: str) -> Dict[str, Any]:
@@ -112,6 +115,29 @@ def _is_event_silenced(event: Dict[str, Any]) -> bool:
return False
def _dedup_ttl_sec(event: Dict[str, Any]) -> int:
payload = event.get("payload") if isinstance(event.get("payload"), dict) else {}
raw = event.get("dedup_ttl_sec", payload.get("dedup_ttl_sec", _DEFAULT_DEDUP_SEC))
try:
return max(0, int(raw or 0))
except (TypeError, ValueError):
return 0
def _is_duplicate_event(event: Dict[str, Any]) -> bool:
ttl = _dedup_ttl_sec(event)
if ttl <= 0:
return False
key = _event_key(event)
now = time.time()
with _DEDUP_LOCK:
until = _EVENT_DEDUP.get(key)
if until and until > now:
return True
_EVENT_DEDUP[key] = now + ttl
return False
def _queue_failed_delivery(
event: Dict[str, Any],
tier: str,
@@ -197,6 +223,20 @@ async def dispatch(event: Dict[str, Any], admin_chat_ids: Optional[list] = None)
"delivered": True,
"silenced": True,
"queued": False,
"deduped": False,
}
if _is_duplicate_event(event):
return {
"tier": tier,
"sent": 0,
"errors": [],
"latency_ms": int((time.perf_counter() - started_at) * 1000),
"payload": {"status": "deduped", "event_key": _event_key(event)},
"delivered": True,
"silenced": False,
"queued": False,
"deduped": True,
}
result = await _run_tier_handler(tier, event, session_id)
@@ -220,6 +260,7 @@ async def dispatch(event: Dict[str, Any], admin_chat_ids: Optional[list] = None)
"delivered": send_result["ok"],
"silenced": False,
"queued": queued,
"deduped": False,
}
except Exception as e:
logger.exception(f"[EventRouter] dispatch failed: {e}")
@@ -233,6 +274,7 @@ async def dispatch(event: Dict[str, Any], admin_chat_ids: Optional[list] = None)
"delivered": False,
"silenced": False,
"queued": queued,
"deduped": False,
}
@@ -286,6 +328,7 @@ def notify_failure(
title: Optional[str] = None,
trace: Optional[str] = None,
payload: Optional[Dict[str, Any]] = None,
dedup_ttl_sec: Optional[int] = None,
) -> Dict[str, Any]:
"""排程/背景任務失敗的同步通知 helper。"""
severity = "alert" if priority in {"P1", "P2"} else "warning"
@@ -300,6 +343,8 @@ def notify_failure(
"trace": trace or "".join(traceback.format_exception(type(error), error, error.__traceback__)),
"payload": {"task_name": task_name, **(payload or {})},
}
if dedup_ttl_sec is not None:
event["dedup_ttl_sec"] = dedup_ttl_sec
return dispatch_sync(event)

View File

@@ -62,6 +62,41 @@ def enqueue_insight_embedding(insight_id: int, insight_type: str, content: str,
return _enqueue_embedding("ai_insights", int(insight_id), embed_target_text)
def enqueue_missing_insight_embeddings(limit: int = 200) -> dict:
"""Backfill existing ai_insights that have not yet entered the embedding queue."""
session = get_session()
try:
rows = session.execute(
text("""
SELECT id, insight_type, period, content
FROM ai_insights i
WHERE i.embedding IS NULL
AND COALESCE(i.status, 'approved') NOT IN ('archived', 'rejected')
AND NOT EXISTS (
SELECT 1
FROM embedding_retry_queue q
WHERE q.target_table = 'ai_insights'
AND q.target_id = i.id
AND q.status IN ('pending', 'processing', 'done')
)
ORDER BY i.created_at DESC
LIMIT :lim
"""),
{"lim": limit},
).fetchall()
except Exception as exc:
sys_log.warning(f"[OCLearn] embedding backfill 略過 (schema/pgvector 未就緒?): {exc}")
return {"scanned": 0, "enqueued": 0, "status": "skipped", "error": str(exc)[:200]}
finally:
session.close()
enqueued = 0
for row in rows:
if enqueue_insight_embedding(row.id, row.insight_type, row.content, row.period):
enqueued += 1
return {"scanned": len(rows), "enqueued": enqueued, "status": "ok"}
def _process_one_embedding(row_id: int, target_table: str, target_id: int,
text_content: str, model: str) -> bool:
"""處理單筆 embedding成功寫回目標表失敗累加 attempts"""