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