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 json
import re
import ssl
import urllib.request
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()
for tag, backend in _BACKENDS.items():
try:
items = await loop.run_in_executor(None, backend, keyword)
except Exception as exc:
data["last_error"] = f"{tag}: {exc}"
had_error = True
items = None
for attempt in range(3):
try:
items = await loop.run_in_executor(None, backend, keyword)
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
seen_set = set(data.get("seen", {}).get(tag, []))

View File

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