← Назад к вопросам

Приведи пример интересной фичи которую реализовал

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

Что было интересно в этом решении

  1. Exponential Backoff — не просто retry, а умная задержка

    Попытка 1: сразу
    Попытка 2: 0.5 сек
    Попытка 3: 1.0 сек
    Попытка 4: 2.0 сек
    Попытка 5: 4.0 сек (максимум 30 сек)
    
  2. Jitter — случайная составляющая предотвращает "thundering herd"

    # Без jitter: 1000 клиентов retry одновременно → еще больше перегруз
    # С jitter: клиенты retry в разные моменты → плавная нагрузка
    
  3. Дифференциация ошибок

    # 400 Bad Request (невалидные данные) → не retry
    # 429 Too Many Requests (перегруз API) → retry
    # 500 Internal Server Error → retry
    # Timeout → retry
    
  4. Метрики для мониторинга

    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

Это был отличный пример, где технические знания решили реальную бизнес-проблему.