# Идемпотентность

Идемпотентность нужна с **обеих** сторон, и это две разные задачи. На отправке —
чтобы повтор запроса не создал второй заказ. На приёме — чтобы повтор вебхука
не выдал доступ дважды.

Обе описаны здесь, потому что забывают обычно вторую.

## Зачем это на отправке

Сеть теряет ответы, а не запросы. Клиент отправил создание платежа, получил
таймаут — и не знает, создался платёж или нет. Оба варианта плохи: повторить —
риск списать дважды, не повторить — риск не списать вовсе.

Ключ идемпотентности снимает выбор: повторять можно всегда.

## Как пользоваться

```bash
curl -X POST 'https://school.example.com/api/v1/orders' \
  -H 'Authorization: Bearer lms_live_a8f3c1e9d2b74a5f8e0c1d3b5a7f9e2c' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: 6f1e2a34-9c8b-4d7e-a1f0-3b5c7d9e1f2a' \
  -H 'Accept: application/json' \
  --data '{ "user_id": "usr_01HQZX3M8K4N2P", "offer_id": "off_01HQZX7QGVPR6A" }'
```

Ключ придумывает клиент. Требование одно: **он должен быть уникален для операции,
а не для попытки**. Все повторы одной и той же операции идут с одним ключом —
в этом весь смысл.

Повтор с тем же ключом возвращает **тот же результат**, что и первый запрос,
и добавляет заголовок:

```
Idempotency-Replayed: true
```

Не создаёт второй заказ, не списывает деньги дважды. Ключ хранится **24 часа**.

### Откуда брать ключ

Хороший ключ — тот, который вы можете воспроизвести после перезапуска своего
процесса. Случайный `uuid`, сгенерированный в момент отправки и не сохранённый,
защищает только от повтора внутри одной попытки — то есть от самого редкого случая.

Практически: генерируйте ключ вместе с задачей и храните его рядом с ней. Если
задача перезапустилась, она берёт тот же ключ.

## Тот же ключ с другим телом

Это ошибка клиента, и отвечаем мы `409` с пояснением.

Молча создать новый объект нельзя: почти наверняка у клиента баг генерации ключей —
например, ключ считается от времени с точностью до секунды, — и лучше он узнает
об этом сразу, чем через месяц по расхождению в отчётах.

## Где ключ обязателен

Там, где повтор опасен: создание заказа, платёж, возврат, выдача доступа, отправка
сообщения.

**Оговорка честности:** в спецификации это сказано прозой, а перечня операций нет.
Поэтому в машинной спецификации `Idempotency-Key` помечен необязательным **везде** —
пометить его обязательным наугад значило бы соврать про контракт. Признак придёт
из кода вместе с эндпоинтом.

Практический совет до этого момента: посылайте ключ на **всех** операциях записи.
Лишним он не будет никогда.

## Идемпотентность на приёме

Вторая половина, и её забывают чаще.

Вебхук может прийти дважды: при повторе после таймаута, при переигрывании
пропущенного, при обрыве после того, как вы уже всё обработали. Поэтому
обработчик обязан быть идемпотентным по `event.id`:

```
1. Взять event.id из конверта.
2. Вставить его в свою таблицу обработанных — с УНИКАЛЬНЫМ индексом.
3. Вставка не прошла: событие уже обработано. Ответить 200 и выйти.
4. Вставка прошла: обработать.
```

Именно уникальный индекс, а не проверка «есть ли такая запись» перед вставкой:
два экземпляра обработчика пройдут проверку одновременно и оба обработают.
Гонка редкая — и потому находится в самый неудобный момент.

То же самое делаем и мы у себя на приёме уведомлений от платёжных провайдеров:
уникальный индекс по паре «провайдер, внешний идентификатор». Повтор уведомления
не создаёт второй платёж и не выдаёт доступ дважды.

## Чего идемпотентность не делает

**Не делает операцию обратимой.** Возврат платежа идемпотентен по ключу, но
необратим по сути: повтор с тем же ключом вернёт тот же результат, а новый ключ
сделает второй возврат.

**Не заменяет проверку состояния.** Отмена уже отменённого заказа даст `409` —
это конфликт состояния, а не задача для ключа.
