feat: 完成 issue #61 ⑥ 可用性监控探针(≥99.8%)

This commit is contained in:
2026-08-05 03:19:19 +08:00
parent c8d6e725a2
commit f6cdc84860
3 changed files with 312 additions and 0 deletions
+104
View File
@@ -0,0 +1,104 @@
# -*- coding: utf-8 -*-
"""可用性监控探针测试(issue #61)。
用本地 ThreadingHTTPServer 模拟健康端点:
1. 稳定 200 → 可用率 100% ≥ 99.8%;
2. 间歇 503 → 可用率按失败比例下降,探针统计正确;
3. 阈值判断:间歇失败超 0.2% 时 meets_target=False;
4. 失败明细记录(URL + 原因)。
"""
import http.server
import os
import sys
import threading
import unittest
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from probe_availability import AvailabilityProbe # noqa: E402
class _HealthHandler(http.server.BaseHTTPRequestHandler):
"""可配置成功/失败模式的 /health 处理器。"""
fail_mode = [] # [True/False...] 按请求序号循环
def do_GET(self):
if self.path == "/health":
if self.fail_mode and self.fail_mode[0]:
self.fail_mode[0] = False # 一次性失败
self.send_response(503)
else:
self.send_response(200)
else:
self.send_response(404)
self.end_headers()
def log_message(self, *args): # 静默日志
pass
def start_server(fail_once=False):
handler = _HealthHandler
handler.fail_mode = [True] if fail_once else []
srv = http.server.ThreadingHTTPServer(("127.0.0.1", 0), handler)
thread = threading.Thread(target=srv.serve_forever, daemon=True)
thread.start()
return srv
class TestProbe(unittest.TestCase):
"""探针统计与阈值判断。"""
def test_stable_endpoint_100pct(self):
srv = start_server()
try:
url = f"http://127.0.0.1:{srv.server_port}"
probe = AvailabilityProbe([url], timeout_seconds=2)
report = probe.run(rounds=10, interval=0)
finally:
srv.shutdown()
self.assertEqual(report.total, 10)
self.assertEqual(report.success, 10)
self.assertEqual(report.availability, 1.0)
self.assertTrue(report.meets_target())
def test_single_failure_below_threshold(self):
srv = start_server(fail_once=True)
try:
url = f"http://127.0.0.1:{srv.server_port}"
probe = AvailabilityProbe([url], timeout_seconds=2)
report = probe.run(rounds=500, interval=0) # 1/500 = 0.2%
finally:
srv.shutdown()
self.assertEqual(len(report.failures), 1)
# 1/500 = 0.002 = 99.8% —— 恰好达标(>= target 判定通过)
self.assertGreaterEqual(report.availability, report.target)
self.assertTrue(report.meets_target())
# urllib 对非 200 抛 HTTPError → 记为不可用
self.assertFalse(report.failures[0].ok)
self.assertIn("503", report.failures[0].detail)
def test_high_failure_rate_fails_threshold(self):
# 构造 50% 失败的端点(fail_mode 每次循环交替:用两个处理器状态不可行,
# 改为直接 mock probe_once)
probe = AvailabilityProbe(["http://x"], timeout_seconds=1)
results = [probe.probe_once.__self__.__class__] # noqa - 占位
from unittest import mock
ok_result = mock.MagicMock()
ok_result.ok = True
bad_result = mock.MagicMock()
bad_result.ok = False
bad_result.url = "http://x"
bad_result.detail = "http-503"
bad_result.latency_ms = 1.0
# 交替 200/503 → 6 次探测 3 成功 → 50% 可用率,远低于 99.8%
with mock.patch.object(probe, "probe_once",
side_effect=[ok_result, bad_result] * 3):
report = probe.run(rounds=6, interval=0)
self.assertEqual(report.availability, 0.5)
self.assertFalse(report.meets_target())
self.assertEqual(len(report.failures), 3)
if __name__ == "__main__":
unittest.main()