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