fix: retry transient SSL/connection errors in alert backends

Add retry loop (3 attempts, exponential backoff) for SSLError,
ConnectionError, TimeoutError, and OSError in alert poll cycle.
Non-transient errors fail immediately. Also fixes searx test
mocks to match direct urlopen usage.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
user
2026-02-15 18:51:28 +01:00
parent f046cced28
commit 6d86e8d7f8
2 changed files with 20 additions and 8 deletions

View File

@@ -5,6 +5,7 @@ from __future__ import annotations
import asyncio import asyncio
import json import json
import re import re
import ssl
import urllib.request import urllib.request
from datetime import datetime, timezone from datetime import datetime, timezone
@@ -271,11 +272,22 @@ async def _poll_once(bot, key: str, announce: bool = True) -> None:
loop = asyncio.get_running_loop() loop = asyncio.get_running_loop()
for tag, backend in _BACKENDS.items(): for tag, backend in _BACKENDS.items():
try: items = None
items = await loop.run_in_executor(None, backend, keyword) for attempt in range(3):
except Exception as exc: try:
data["last_error"] = f"{tag}: {exc}" items = await loop.run_in_executor(None, backend, keyword)
had_error = True break
except (ssl.SSLError, ConnectionError, TimeoutError, OSError) as exc:
if attempt < 2:
await asyncio.sleep(2 ** attempt)
continue
data["last_error"] = f"{tag}: {exc}"
had_error = True
except Exception as exc:
data["last_error"] = f"{tag}: {exc}"
had_error = True
break
if items is None:
continue continue
seen_set = set(data.get("seen", {}).get(tag, [])) seen_set = set(data.get("seen", {}).get(tag, []))

View File

@@ -1220,7 +1220,7 @@ class TestSearchSearx:
def close(self): def close(self):
pass pass
with patch.object(_mod, "_urlopen", return_value=FakeResp()): with patch("urllib.request.urlopen", return_value=FakeResp()):
results = _search_searx("test query") results = _search_searx("test query")
assert len(results) == 3 assert len(results) == 3
assert results[0]["id"] == "https://example.com/sx1" assert results[0]["id"] == "https://example.com/sx1"
@@ -1237,13 +1237,13 @@ class TestSearchSearx:
def close(self): def close(self):
pass pass
with patch.object(_mod, "_urlopen", return_value=FakeResp()): with patch("urllib.request.urlopen", return_value=FakeResp()):
results = _search_searx("nothing") results = _search_searx("nothing")
assert results == [] assert results == []
def test_http_error_propagates(self): def test_http_error_propagates(self):
import pytest import pytest
with patch.object(_mod, "_urlopen", side_effect=ConnectionError("fail")): with patch("urllib.request.urlopen", side_effect=ConnectionError("fail")):
with pytest.raises(ConnectionError): with pytest.raises(ConnectionError):
_search_searx("test") _search_searx("test")