fix(python): make retries safe and deterministic
This commit is contained in:
1 parent
f64d00673c
commit
82e5807752
8 files changed
+567
-50
No files matched your search
@@ -0,0 +1,95 @@
|
||||
"""Shared retry policy for the synchronous and asynchronous clients."""
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from email.utils import parsedate_to_datetime
|
||||
from logging import Logger
|
||||
from typing import Optional
|
||||
|
||||
IDEMPOTENT_METHODS = frozenset({"DELETE", "GET", "HEAD", "OPTIONS", "PUT"})
|
||||
RETRYABLE_STATUS_CODES = frozenset({408, 429, 500, 502, 503, 504})
|
||||
MAX_BACKOFF_SECONDS = 8
|
||||
|
||||
|
||||
def require_max_retries(value: object) -> int:
|
||||
if isinstance(value, bool) or not isinstance(value, int) or value < 0:
|
||||
raise ValueError("max_retries must be a non-negative integer")
|
||||
return value
|
||||
|
||||
|
||||
def can_retry(
|
||||
method: str,
|
||||
retry_count: int,
|
||||
max_retries: int,
|
||||
*,
|
||||
request_known_unprocessed: bool = False,
|
||||
) -> bool:
|
||||
if retry_count >= max_retries:
|
||||
return False
|
||||
return request_known_unprocessed or method.upper() in IDEMPOTENT_METHODS
|
||||
|
||||
|
||||
def should_retry_response(
|
||||
method: str,
|
||||
status_code: int,
|
||||
retry_count: int,
|
||||
max_retries: int,
|
||||
) -> bool:
|
||||
return status_code in RETRYABLE_STATUS_CODES and can_retry(
|
||||
method, retry_count, max_retries
|
||||
)
|
||||
|
||||
|
||||
def retry_delay_seconds(
|
||||
retry_count: int,
|
||||
retry_after: Optional[str] = None,
|
||||
*,
|
||||
now: Optional[datetime] = None,
|
||||
) -> float:
|
||||
parsed_delay = _parse_retry_after(retry_after, now=now)
|
||||
if parsed_delay is not None:
|
||||
return parsed_delay
|
||||
return float(min(2**retry_count, MAX_BACKOFF_SECONDS))
|
||||
|
||||
|
||||
def log_retry(
|
||||
logger: Logger,
|
||||
method: str,
|
||||
path: str,
|
||||
retry_count: int,
|
||||
max_retries: int,
|
||||
delay: float,
|
||||
reason: str,
|
||||
) -> None:
|
||||
logger.warning(
|
||||
"Retrying Elydora request method=%s path=%s retry=%d/%d "
|
||||
"delay_seconds=%s reason=%s",
|
||||
method.upper(),
|
||||
path,
|
||||
retry_count + 1,
|
||||
max_retries,
|
||||
delay,
|
||||
reason,
|
||||
)
|
||||
|
||||
|
||||
def _parse_retry_after(
|
||||
value: Optional[str],
|
||||
*,
|
||||
now: Optional[datetime],
|
||||
) -> Optional[float]:
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
normalized = value.strip()
|
||||
if normalized.isascii() and normalized.isdecimal():
|
||||
return float(int(normalized))
|
||||
|
||||
try:
|
||||
retry_at = parsedate_to_datetime(normalized)
|
||||
except (TypeError, ValueError, OverflowError):
|
||||
return None
|
||||
if retry_at.tzinfo is None:
|
||||
retry_at = retry_at.replace(tzinfo=timezone.utc)
|
||||
|
||||
current_time = now or datetime.now(timezone.utc)
|
||||
return max(0.0, (retry_at - current_time).total_seconds())
|
||||
Reference in new issue
Block a user