# Асинхронное взаимодействие и очереди

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

## Цель

Углубить понимание **асинхронных** паттернов: постановка задач в очередь, polling, webhooks, гарантии доставки и согласованность с БД.

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

- [Синхронные вызовы](sinhronnye-vyzovy.md)
- [Очереди и события](../02-komponenty/ocheredi-i-sobytiya.md)

## Время

**3–4 часа**

---

## Асинхрон vs синхрон — решение

| Критерий | Sync | Async |
|----------|------|-------|
| UX «сразу видел результат» | ✓ | Частично (polling) |
| Длинная работа (>500 ms) | ✗ | ✓ |
| Пики нагрузки | Может уронить API | Сглаживает очередью |
| Отладка | Проще стек | Нужны correlation id |

---

## Паттерн «принять — обработать позже»

```mermaid
sequenceDiagram
  participant C as Клиент
  participant API as API
  participant Q as Очередь
  participant W as Worker
  C->>API: POST /invites
  API->>API: validate + save DB
  API->>Q: enqueue job
  API-->>C: 202 { job_id }
  W->>Q: consume
  W->>W: send email
```

Клиент может **poll** `GET /jobs/{id}` или получить push, когда готово.

---

## Webhook — обратный async

Внешний SaaS **сам** вызывает ваш URL, когда событие готово (платёж прошёл).

| Сторона | Действие |
|---------|----------|
| Вы | Регистрируете `https://api.example.com/webhooks/stripe` |
| SaaS | POST подписанное тело при `payment.succeeded` |
| Вы | Проверяете подпись, обновляете заказ |

**Не путать** с очередью внутри вашей системы — это ingress HTTP, нужна **идемпотентность** по `event_id`.

---

## Correlation и job id

Каждое async-сообщение несёт:

| Поле | Зачем |
|------|-------|
| `message_id` | Уникальность |
| `correlation_id` | Связь с HTTP-запросом в логах |
| `causation_id` | Цепочка событий |

Без них support не найдёт «письмо не пришло» по тикету пользователя.

---

## Backpressure

Если producers быстрее consumers, очередь **растёт**.

| Симптом | Действие |
|---------|----------|
| Depth ↑ 24ч | Больше workers, оптимизация handler |
| DLQ растёт | Фикс бага, replay после патча |
| Memory queue | Перейти на persistent broker |

Архитектор закладывает **макс. глубину** и алерт в NFR.

---

## Согласованность: transactional outbox

Проблема: commit в БД прошёл, publish в queue — нет.

**Решение outbox** (повтор из [компонентов](../02-komponenty/ocheredi-i-sobytiya.md)):

1. Таблица `outbox(id, payload, created_at)`  
2. Транзакция: business row + outbox row  
3. Relay процесс: read outbox → publish → mark sent  

Альтернатива хуже: **dual write** без транзакции — race и потери.

---

## Повторная доставка и идемпотентность

At-least-once → worker может получить дубликат.

```text
Обработка invite_id=7:
  если уже sent_at IS NOT NULL → skip
  иначе send + UPDATE sent_at
```

Таблица `processed_messages(message_id)` — ещё один вариант.

---

## Scheduled / delayed jobs

| Задача | Механизм |
|--------|----------|
| Напоминание через 24ч | Delayed queue, cron scan |
| Отчёт раз в сутки | CronJob в K8s + идемпотентный run |

Не смешивайте «каждые 5 мин всё сканировать» с event-driven без нужды.

---

## Async API контракт

Документируйте в OpenAPI:

| Endpoint | Ответ |
|----------|-------|
| POST /tasks | 202 + `task_id` |
| GET /tasks/{id}` | `pending | running | done | failed` |

Пользователь понимает, что операция **не мгновенная**.

---

## Практика

1. Спроектируйте таблицу `outbox` для InviteCreated.  
2. Опишите идемпотентность email worker (псевдокод).  
3. Webhook от платежей: какие заголовки проверяете?

---

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

1. Зачем correlation_id в сообщении очереди?  
2. Чем webhook отличается от consumer очереди?  
3. Почему dual write опасен?

---

## Дальше

→ [Event-driven архитектура](event-driven.md)
