← Назад к вопросам
Приведи пример интересной фичи которую реализовал
1.3 Junior🔥 221 комментариев
#Python Core
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Интересная фича: Smart Retry System с Exponential Backoff
Очень люблю рассказывать об этом проекте — это был отличный пример, где я смог применить знания о асинхронности, архитектуре и решить реальную бизнес-проблему.
Контекст задачи
В одном из проектов мы интегрировались с внешними платежными API (Stripe, Yandex.Kassa). Проблема была в следующем:
- Иногда API временно недоступен (network timeout, 5xx ошибки)
- Мы теряли целые транзакции из-за одного сбоя
- Manually retry было непредсказуемо и приводило к дублям
- Логирование было хаотичным, сложно было отследить проблему
Решение v1: Простой retry
Сначала я сделал базовую версию:
# ❌ Первая попытка — слишком наивно
import requests
def process_payment(payment_data):
max_retries = 3
for attempt in range(max_retries):
try:
response = requests.post(
"https://api.stripe.com/v1/charges",
data=payment_data,
timeout=5
)
response.raise_for_status()
return response.json()
except requests.RequestException as e:
if attempt == max_retries - 1:
raise
# Просто повторяем сразу — ПЛОХО!
# Если API перегружена, это только усугубит ситуацию
continue
Проблемы с v1:
- Без задержки между retry — API ещё больше перегружается
- Нет логирования попыток
- Нет дифференциации ошибок (4xx ошибки не нужно retry)
- Код рассеян по приложению
Решение v2: Exponential Backoff с конфигурацией
# ✅ Финальная версия
import asyncio
import logging
from typing import Callable, Any, Dict, Optional
from dataclasses import dataclass
import aiohttp
import json
logger = logging.getLogger(__name__)
@dataclass
class RetryConfig:
"""Конфигурация стратегии повтора"""
max_attempts: int = 3
initial_delay: float = 1.0 # секунды
max_delay: float = 60.0 # максимальная задержка
backoff_factor: float = 2.0 # экспоненциальный множитель
jitter: bool = True # добавлять ли случайность
retryable_status_codes: set = None # какие коды retry
def __post_init__(self):
if self.retryable_status_codes is None:
# Retry только на временные ошибки
self.retryable_status_codes = {408, 429, 500, 502, 503, 504}
class SmartRetryError(Exception):
"""Ошибка после исчерпания всех попыток retry"""
def __init__(self, message: str, last_exception: Exception, attempts: int):
self.message = message
self.last_exception = last_exception
self.attempts = attempts
super().__init__(message)
class SmartRetryHandler:
"""Умная система повтора с логированием и анализом ошибок"""
def __init__(self, config: RetryConfig = None):
self.config = config or RetryConfig()
self.metrics = {}
def _calculate_delay(self, attempt: int) -> float:
"""Расчёт задержки с exponential backoff"""
delay = self.config.initial_delay * (self.config.backoff_factor ** attempt)
delay = min(delay, self.config.max_delay)
if self.config.jitter:
import random
# Добавляем случайность от 0 до 25% для избежания thundering herd
jitter = delay * random.uniform(0, 0.25)
delay += jitter
return delay
def _is_retryable(self, exception: Exception, status_code: Optional[int] = None) -> bool:
"""Определяем, стоит ли retry для этой ошибки"""
# 4xx ошибки (кроме 429) не стоит retry
if status_code and 400 <= status_code < 500 and status_code != 429:
return False
# Сетевые ошибки стоит retry
if isinstance(exception, (aiohttp.ClientConnectorError, asyncio.TimeoutError)):
return True
# 5xx ошибки стоит retry
if status_code and status_code >= 500:
return True
return False
async def execute(
self,
func: Callable,
*args,
**kwargs
) -> Any:
"""Выполняем функцию с умным retry"""
last_exception = None
for attempt in range(self.config.max_attempts):
try:
logger.info(
f"Executing {func.__name__}, attempt {attempt + 1}/{self.config.max_attempts}"
)
result = await func(*args, **kwargs)
if attempt > 0:
logger.info(
f"{func.__name__} succeeded after {attempt} retries"
)
self._record_metric(func.__name__, "success_after_retry", attempt)
return result
except Exception as e:
last_exception = e
status_code = getattr(e, "status_code", None)
if not self._is_retryable(e, status_code):
logger.error(
f"{func.__name__} failed with non-retryable error: {e}"
)
self._record_metric(func.__name__, "failed_non_retryable")
raise
if attempt == self.config.max_attempts - 1:
logger.error(
f"{func.__name__} failed after {self.config.max_attempts} attempts: {e}"
)
self._record_metric(func.__name__, "failed_max_attempts")
raise SmartRetryError(
f"Failed after {self.config.max_attempts} attempts",
last_exception,
self.config.max_attempts
)
# Ждём перед следующей попыткой
delay = self._calculate_delay(attempt)
logger.warning(
f"{func.__name__} failed (attempt {attempt + 1}), "
f"retrying in {delay:.2f}s: {e}"
)
self._record_metric(func.__name__, "retry", attempt)
await asyncio.sleep(delay)
def _record_metric(self, func_name: str, metric_type: str, attempt: int = 0):
"""Запись метрик для анализа"""
key = f"{func_name}_{metric_type}"
self.metrics[key] = self.metrics.get(key, 0) + 1
def get_metrics(self) -> Dict[str, int]:
"""Получить метрики для мониторинга"""
return self.metrics.copy()
Использование в приложении
# Инициализируем retry handler один раз
payment_retry_config = RetryConfig(
max_attempts=3,
initial_delay=0.5,
max_delay=30.0,
backoff_factor=2.0,
jitter=True
)
retry_handler = SmartRetryHandler(payment_retry_config)
async def charge_payment(user_id: str, amount: float) -> Dict[str, Any]:
"""Асинхронная функция для обработки платежа"""
async with aiohttp.ClientSession() as session:
async with session.post(
"https://api.stripe.com/v1/charges",
json={
"amount": int(amount * 100),
"currency": "usd",
"customer": user_id
},
timeout=aiohttp.ClientTimeout(total=10)
) as response:
if response.status >= 400:
raise PaymentAPIError(f"Status {response.status}")
return await response.json()
async def process_payment_request(user_id: str, amount: float):
"""Основной handler который использует retry"""
try:
result = await retry_handler.execute(
charge_payment,
user_id=user_id,
amount=amount
)
logger.info(f"Payment succeeded for user {user_id}: {result}")
return result
except SmartRetryError as e:
# После всех попыток — отправляем в очередь для ручного разбора
await send_to_manual_review_queue(user_id, amount)
logger.error(f"Payment failed for {user_id} after retries", exc_info=e.last_exception)
raise
Что было интересно в этом решении
-
Exponential Backoff — не просто retry, а умная задержка
Попытка 1: сразу Попытка 2: 0.5 сек Попытка 3: 1.0 сек Попытка 4: 2.0 сек Попытка 5: 4.0 сек (максимум 30 сек) -
Jitter — случайная составляющая предотвращает "thundering herd"
# Без jitter: 1000 клиентов retry одновременно → еще больше перегруз # С jitter: клиенты retry в разные моменты → плавная нагрузка -
Дифференциация ошибок
# 400 Bad Request (невалидные данные) → не retry # 429 Too Many Requests (перегруз API) → retry # 500 Internal Server Error → retry # Timeout → retry -
Метрики для мониторинга
metrics = retry_handler.get_metrics() # { # "charge_payment_success_after_retry": 120, # "charge_payment_retry": 240, # "charge_payment_failed_max_attempts": 2 # } # Видим тренд: много retry → API проблемы?
Результаты
До реализации:
- Потеря ~2-3% платежей из-за временных сбоев
- Ручное разбирательство каждого случая
- ~5-10 часов в неделю на работу с проблемами
- Сложный откат и дублирование платежей
После реализации:
- Успешная обработка 99.7% платежей
- Автоматический retry без потерь
- ~30 минут в неделю на мониторинг
- Прозрачные логи для анализа
- Метрики в систему мониторинга для алертов
Эта фича показала мне важность:
- Асинхронного программирования в Python
- Graceful degradation в распределённых системах
- Интеграции с внешними сервисами
- Мониторинга и observability
Это был отличный пример, где технические знания решили реальную бизнес-проблему.