From d46e6018638ecc5db17039fca788ba4dc4d4ccd4 Mon Sep 17 00:00:00 2001 From: kevin9327 Date: Sat, 5 Sep 2026 21:14:53 +0900 Subject: [PATCH] =?UTF-8?q?fix(notifications):=20=E9=A3=9E=E4=B9=A6?= =?UTF-8?q?=E6=8E=A8=E9=80=81=E6=8C=89=E4=BC=A0=E5=85=A5=E7=9A=84=20max=5F?= =?UTF-8?q?attempts=20=E9=80=80=E9=81=BF=E4=B8=8E=E8=AE=B0=E6=97=A5?= =?UTF-8?q?=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _post_feishu 的退避判断与最终 WARNING 仍用模块常量 _FEISHU_MAX_ATTEMPTS(3), 设置页「发送测试消息」传 max_attempts=1 时: 唯一一次失败后仍 sleep 1 秒才返回, 日志写「已重试 3 次」而实际只试了 1 次。两处改用 max_attempts; 生产路径不变。 测试: 单次尝试不退避且日志计数为 1 (未修复时失败), 默认 3 次仍退避 1s、2s。 --- backend/app/services/webhook_adapter.py | 4 +-- backend/tests/test_notification_adapters.py | 40 +++++++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/backend/app/services/webhook_adapter.py b/backend/app/services/webhook_adapter.py index 8d6fa7b..ca21139 100644 --- a/backend/app/services/webhook_adapter.py +++ b/backend/app/services/webhook_adapter.py @@ -141,10 +141,10 @@ def _post_feishu(webhook_url: str, payload: dict, secret: str, max_attempts: int except Exception as e: # noqa: BLE001 — 网络/超时, 可重试 last_err = str(e) - if attempt < _FEISHU_MAX_ATTEMPTS: + if attempt < max_attempts: time.sleep(min(2 ** (attempt - 1), 3)) # 退避: 1s, 2s - logger.warning("飞书 Webhook 推送最终失败(已重试 %d 次): %s", _FEISHU_MAX_ATTEMPTS, last_err) + logger.warning("飞书 Webhook 推送最终失败(已重试 %d 次): %s", max_attempts, last_err) return False diff --git a/backend/tests/test_notification_adapters.py b/backend/tests/test_notification_adapters.py index 7f08507..44e0ba4 100644 --- a/backend/tests/test_notification_adapters.py +++ b/backend/tests/test_notification_adapters.py @@ -3,6 +3,7 @@ from __future__ import annotations import hashlib import hmac import json +import logging from app.services import email_adapter, webhook_adapter @@ -98,3 +99,42 @@ def test_email_adapter_uses_starttls_login_and_multiple_recipients(monkeypatch): assert message["To"] == "one@example.com, two@example.com" assert message["Subject"] == "监控告警" assert smtp.calls[-1] == ("quit",) + + +def _unreachable(*_args, **_kwargs): + raise ConnectionError("unreachable") + + +def test_feishu_single_attempt_returns_without_backoff_and_logs_the_real_count(monkeypatch, caplog): + # 设置页「发送测试消息」传 max_attempts=1: 失败即返回, 不等退避, 日志计数如实。 + sleeps: list[float] = [] + monkeypatch.setattr(webhook_adapter.time, "sleep", lambda seconds: sleeps.append(seconds)) + monkeypatch.setattr("httpx.post", _unreachable) + with caplog.at_level(logging.WARNING, logger=webhook_adapter.__name__): + ok = webhook_adapter.send_feishu( + "https://open.feishu.cn/open-apis/bot/v2/hook/abc", "标题", "正文", max_attempts=1 + ) + assert ok is False + assert sleeps == [] + assert "已重试 1 次" in caplog.text + + +def test_feishu_production_retries_keep_backoff(monkeypatch, caplog): + # 生产路径 (默认 3 次) 的退避与日志不变: 1s、2s 两次退避, 日志写 3 次。 + sleeps: list[float] = [] + calls = {"n": 0} + + def unreachable(*_args, **_kwargs): + calls["n"] += 1 + raise ConnectionError("unreachable") + + monkeypatch.setattr(webhook_adapter.time, "sleep", lambda seconds: sleeps.append(seconds)) + monkeypatch.setattr("httpx.post", unreachable) + with caplog.at_level(logging.WARNING, logger=webhook_adapter.__name__): + ok = webhook_adapter.send_feishu( + "https://open.feishu.cn/open-apis/bot/v2/hook/abc", "标题", "正文" + ) + assert ok is False + assert calls["n"] == 3 + assert sleeps == [1, 2] + assert "已重试 3 次" in caplog.text