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:
@@ -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, []))
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
Reference in New Issue
Block a user