From f85eca11ca59de75c3a63b5232cd0c8ba3897753 Mon Sep 17 00:00:00 2001 From: sinohqb Date: Fri, 14 Aug 2026 14:54:57 +0800 Subject: [PATCH] =?UTF-8?q?fix(alerts):=20webhook=20retry=20+=20dedupe=20(?= =?UTF-8?q?resolves=20=C2=A76.2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _send_webhook: - Dedupe: skip if alert.webhook_sent is already True (guards against repeated check_alerts ticks re-sending the same alert). - Retry: up to 3 attempts with exponential backoff (1s, 2s) before giving up. webhook_sent=True is persisted only on success. Two xfail guards in test_openclaw_client_and_webhook now pass (876/4 xfail). --- backend/agenteval/intelligent_eval/alerts.py | 56 ++++++++++++------- .../unit/test_openclaw_client_and_webhook.py | 15 ----- 2 files changed, 37 insertions(+), 34 deletions(-) diff --git a/backend/agenteval/intelligent_eval/alerts.py b/backend/agenteval/intelligent_eval/alerts.py index 6d2b395..9965633 100644 --- a/backend/agenteval/intelligent_eval/alerts.py +++ b/backend/agenteval/intelligent_eval/alerts.py @@ -151,31 +151,49 @@ class AlertManager: return alert - def _send_webhook(self, alert: AlertHistoryDB) -> None: - """Send webhook notification.""" + def _send_webhook(self, alert: AlertHistoryDB, max_retries: int = 3) -> None: + """Send webhook with retry and dedupe. + + P1 真问题修复(§6.2): + - **Dedupe**: if ``alert.webhook_sent`` is already True, skip — guards + against repeated calls for the same alert (e.g. consecutive + ``check_alerts`` ticks). + - **Retry**: attempt the POST up to ``max_retries`` times with + exponential backoff (1s, 2s) before giving up. Persist + ``webhook_sent=True`` only on success. + """ if not self.webhook_url: return + if alert.webhook_sent: + return - try: - payload = { - "alert_id": alert.id, - "alert_type": alert.alert_type, - "severity": alert.severity, - "message": alert.message, - "metric_value": alert.metric_value, - "threshold": alert.threshold, - "timestamp": alert.created_at.isoformat(), - } + payload = { + "alert_id": alert.id, + "alert_type": alert.alert_type, + "severity": alert.severity, + "message": alert.message, + "metric_value": alert.metric_value, + "threshold": alert.threshold, + "timestamp": alert.created_at.isoformat(), + } - response = httpx.post(self.webhook_url, json=payload, timeout=5.0) - response.raise_for_status() + import time - alert.webhook_sent = True - self.session.commit() + last_err: Exception | None = None + for attempt in range(max_retries): + try: + response = httpx.post(self.webhook_url, json=payload, timeout=5.0) + response.raise_for_status() + alert.webhook_sent = True + self.session.commit() + _logger.info(f"Webhook sent for alert {alert.id} (attempt {attempt + 1})") + return + except Exception as e: + last_err = e + if attempt < max_retries - 1: + time.sleep(2 ** attempt) - _logger.info(f"Webhook sent for alert {alert.id}") - except Exception as e: - _logger.error(f"Failed to send webhook: {e}") + _logger.error(f"Failed to send webhook for alert {alert.id} after {max_retries} attempts: {last_err}") def get_alert_history(self, limit: int = 100) -> list[AlertHistoryDB]: """Get alert history. diff --git a/tests/unit/test_openclaw_client_and_webhook.py b/tests/unit/test_openclaw_client_and_webhook.py index 4d43f6b..ad65e3b 100644 --- a/tests/unit/test_openclaw_client_and_webhook.py +++ b/tests/unit/test_openclaw_client_and_webhook.py @@ -139,14 +139,6 @@ def test_webhook_failure_does_not_raise_or_mark_sent(db_session: Session): assert alert.webhook_sent is False -@pytest.mark.xfail( - reason=( - "Known gap: webhook has no retry. _send_webhook swallows the first " - "failure and leaves webhook_sent=False; no follow-up attempt is made. " - "Tracked in .scratch/v111-architecture-scan.md." - ), - strict=False, -) def test_webhook_retries_on_failure(db_session: Session): alert = _make_alert(db_session) fake_resp = MagicMock(); fake_resp.raise_for_status = MagicMock() @@ -160,13 +152,6 @@ def test_webhook_retries_on_failure(db_session: Session): assert alert.webhook_sent is True -@pytest.mark.xfail( - reason=( - "Known gap: no dedupe — every check_alerts trigger re-sends webhook " - "for the same alert. Tracked in .scratch/v111-architecture-scan.md." - ), - strict=False, -) def test_webhook_dedupes_repeat_triggers(db_session: Session): """Calling _send_webhook twice on the same alert must POST at most once.""" alert = _make_alert(db_session)