fix: centralize retry logic in proxy transport layer

Add exponential-backoff retry (3 attempts) for transient SSL,
connection, timeout, and OS errors to all three proxy functions:
urlopen, create_connection, open_connection. Remove per-plugin
retry from alert.py since transport layer now handles it.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
user
2026-02-15 18:55:21 +01:00
parent 6d86e8d7f8
commit 118cf0de21
2 changed files with 53 additions and 27 deletions

View File

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

View File

@@ -1,8 +1,10 @@
"""Proxy-aware HTTP/TCP helpers -- routes outbound traffic through SOCKS5."""
import asyncio
import logging
import socket
import ssl
import time
import urllib.request
import socks
@@ -11,6 +13,10 @@ from sockshandler import SocksiPyConnectionS, SocksiPyHandler
_PROXY_ADDR = "127.0.0.1"
_PROXY_PORT = 1080
_MAX_RETRIES = 3
_RETRY_ERRORS = (ssl.SSLError, ConnectionError, TimeoutError, OSError)
_log = logging.getLogger(__name__)
class _ProxyHandler(SocksiPyHandler, urllib.request.HTTPSHandler):
@@ -35,13 +41,25 @@ class _ProxyHandler(SocksiPyHandler, urllib.request.HTTPSHandler):
def urlopen(req, *, timeout=None, context=None):
"""Proxy-aware drop-in for urllib.request.urlopen."""
"""Proxy-aware drop-in for urllib.request.urlopen.
Retries on transient SSL/connection errors with exponential backoff.
"""
handler = _ProxyHandler(context=context)
opener = urllib.request.build_opener(handler)
kwargs = {}
if timeout is not None:
kwargs["timeout"] = timeout
return opener.open(req, **kwargs)
for attempt in range(_MAX_RETRIES):
try:
return opener.open(req, **kwargs)
except _RETRY_ERRORS as exc:
if attempt + 1 >= _MAX_RETRIES:
raise
delay = 2 ** attempt
_log.debug("urlopen retry %d/%d after %s: %s",
attempt + 1, _MAX_RETRIES, type(exc).__name__, exc)
time.sleep(delay)
def build_opener(*handlers, context=None):
@@ -54,20 +72,31 @@ def create_connection(address, *, timeout=None):
"""SOCKS5-proxied drop-in for socket.create_connection.
Returns a connected socksocket (usable as context manager).
Retries on transient connection errors with exponential backoff.
"""
host, port = address
sock = socks.socksocket()
sock.set_proxy(SOCKS5, _PROXY_ADDR, _PROXY_PORT, rdns=True)
if timeout is not None:
sock.settimeout(timeout)
sock.connect((host, port))
return sock
for attempt in range(_MAX_RETRIES):
try:
sock = socks.socksocket()
sock.set_proxy(SOCKS5, _PROXY_ADDR, _PROXY_PORT, rdns=True)
if timeout is not None:
sock.settimeout(timeout)
sock.connect((host, port))
return sock
except _RETRY_ERRORS as exc:
if attempt + 1 >= _MAX_RETRIES:
raise
delay = 2 ** attempt
_log.debug("create_connection retry %d/%d after %s: %s",
attempt + 1, _MAX_RETRIES, type(exc).__name__, exc)
time.sleep(delay)
async def open_connection(host, port, *, timeout=None):
"""SOCKS5-proxied drop-in for asyncio.open_connection.
SOCKS5 handshake runs in a thread executor; returns (reader, writer).
Retries on transient connection errors with exponential backoff.
"""
loop = asyncio.get_running_loop()
@@ -80,5 +109,14 @@ async def open_connection(host, port, *, timeout=None):
sock.setblocking(False)
return sock
sock = await loop.run_in_executor(None, _connect)
return await asyncio.open_connection(sock=sock)
for attempt in range(_MAX_RETRIES):
try:
sock = await loop.run_in_executor(None, _connect)
return await asyncio.open_connection(sock=sock)
except _RETRY_ERRORS as exc:
if attempt + 1 >= _MAX_RETRIES:
raise
delay = 2 ** attempt
_log.debug("open_connection retry %d/%d after %s: %s",
attempt + 1, _MAX_RETRIES, type(exc).__name__, exc)
await asyncio.sleep(delay)