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:
@@ -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