← [Раздел](README.md) · [Главная](../README.md)

# Circuit breaker и retry

## Цель

Освоить паттерны **повторных попыток (retry)**, **экспоненциальной задержки (backoff)** и **автоматического выключателя (circuit breaker)**, чтобы сбой одного сервиса не обрушил всю цепочку вызовов.

## Предварительно

- [README раздела 07](README.md)
- Понимание синхронных HTTP-вызовов между сервисами
- Базовые коды HTTP: 429, 500, 503

## Время

~60 минут + 20 минут на разбор псевдокода

---

## Проблема: каскадный сбой

```text
Сервис A → Сервис B (медленный/упал) → A копит потоки → A падает → C падает
```

Это **cascade failure**. Пользователь видит ошибку в сервисе A, хотя корень — в B.

Задача архитектора: **ограничить распространение** сбоя и дать системе время восстановиться.

---

## Retry — когда и как

**Retry** — повторить запрос после ошибки или таймаута.

| Повторять | Не повторять |
|-----------|--------------|
| 503 Service Unavailable | 400 Bad Request |
| Сетевой таймаут | 401 / 403 |
| 429 с Retry-After | Успешный 200 с дубликатом заказа* |

\*Если операция **не идемпотентна**, слепой retry создаёт двойной заказ. Решение: **idempotency key** в заголовке (`Idempotency-Key: uuid`), сервер хранит результат по ключу 24 ч.

### Exponential backoff + jitter

```text
Попытка 1: сразу
Попытка 2: ~200 мс + случайный разброс
Попытка 3: ~400 мс + jitter
Попытка 4: ~800 мс …
Максимум попыток: 3–5
```

**Jitter** (случайность) не даёт всем клиентам ударить в B одновременно после восстановления — «thundering herd».

### Антипаттерны retry

| Плохо | Почему |
|-------|--------|
| Бесконечные retry | Убивает перегруженный сервис |
| Retry на POST без idempotency | Дубликаты данных |
| Одинаковый timeout на все зависимости | Медленная цепочка блокирует пул потоков |

Задайте **короткий timeout** на вызов B и **общий deadline** на запрос пользователя (context deadline).

---

## Circuit breaker — «предохранитель»

Три состояния:

```mermaid
stateDiagram-v2
  [*] --> Closed
  Closed --> Open: ошибки > порога
  Open --> HalfOpen: таймер истёк
  HalfOpen --> Closed: пробный запрос OK
  HalfOpen --> Open: пробный запрос fail
```

| Состояние | Поведение |
|-----------|-----------|
| **Closed** | Запросы идут нормально, считаем ошибки |
| **Open** | Сразу fail fast, не нагружаем B |
| **Half-Open** | Пропускаем один пробный запрос |

**Fail fast** в состоянии Open освобождает ресурсы A и даёт B восстановиться.

Параметры (пример для учебного проекта):

- Порог: 50% ошибок за 10 с
- Open duration: 30 с
- Half-open: 1–3 пробных запроса

Библиотеки: Resilience4j (Java), Polly (.NET), `cockatiel` / обёртки в Go, Envoy/Istio на уровне mesh.

---

## Bulkhead — изоляция ресурсов

**Bulkhead** (переборка на корабле): отдельные пулы потоков/соединений для разных зависимостей.

```text
Пул для БД: 20 соединений
Пул для «Рекомендаций»: 5 соединений  ← если рекомендации зависли, каталог жив
```

Без bulkhead один медленный downstream съедает весь пул HTTP-клиента.

---

## Timeout budget

Если пользователь ждёт ответ не больше **2 с**, распределите бюджет:

| Шаг | Бюджет |
|-----|--------|
| A обрабатывает | 200 мс |
| Вызов B | 800 мс |
| Вызов C | 500 мс |
| Запас | 500 мс |

Сумма не должна превышать SLA endpoint'а. В распределённых системах передавайте **оставшееся время** в заголовке (например `X-Request-Deadline`).

---

## Fallback и graceful degradation

Когда circuit open или timeout:

- Вернуть кэшированный ответ («цена может быть неактуальна»).
- Вернуть упрощённый ответ (пустой список рекомендаций).
- Поставить задачу в очередь и ответить 202 Accepted.

Лучше предсказуемая деградация, чем 500 без объяснения.

---

## Где настраивать

| Уровень | Плюсы |
|---------|-------|
| **В коде приложения** | Тонкий контроль, тесты |
| **Sidecar / service mesh** | Единая политика для всех сервисов |
| **API Gateway** | Быстрый старт без изменения кода |

Для курса достаточно понимать логику в коде; в проде часто комбинируют gateway + библиотеку.

---

## Мини-пример (псевдокод)

```python
def get_recommendations(user_id, deadline):
    breaker = breakers["recommendations"]
    if breaker.is_open():
        return cached_or_empty(user_id)

    for attempt in range(3):
        try:
            return http_get(
                f"https://rec.example.com/users/{user_id}",
                timeout=remaining(deadline),
                headers={"Idempotency-Key": new_uuid()},
            )
        except TransientError:
            sleep(backoff_with_jitter(attempt))
    breaker.record_failure()
    return cached_or_empty(user_id)
```

---

## Самопроверка

1. Что такое cascade failure?
2. Зачем idempotency key при retry POST?
3. Опишите три состояния circuit breaker.
4. Чем bulkhead помогает при медленной зависимости?

---

## Дальше

→ [Раздел 08 — Observability](../08-observability/README.md)  
← [Резервирование и DR](rezervirovanie-i-dr.md)
