Ключи идемпотентности и честное списание в API генерации изображений
2 мин чтенияАвтор Unified Image API Team
У любого метрируемого API один и тот же кошмарный сценарий: клиент ловит таймаут, повторяет запрос — и платит дважды за одну работу. Или наоборот: провайдер падает посреди вызова, а клиент платит ни за что. Этот пост — о гарантиях, которые мы даём, как они устроены и что благодаря им можно удалить из вашего кода.
Гарантия
Один кредит списывается максимум за одно доставленное изображение. Конкретно:
- Отправка атомарно резервирует стоимость задания (условный single-row update — баланс не может уйти в минус, а два конкурентных запроса не потратят одни и те же последние кредиты).
- Неудачное или отменённое задание автоматически возвращает резерв. Строка в леджере уникальна по паре (задание, тип) — сам возврат тоже exactly-once, даже если его попытаются сделать два воркера.
- Вызов модели записывается заранее (write-ahead): прежде чем звать провайдера, мы фиксируем попытку; если процесс умирает посреди вызова, сверка разрешает «неизвестное» состояние по данным провайдера, а не наугад. Ни один неизвестный вызов не тарифицируется молча.
Ключи идемпотентности закрывают клиентскую половину
Серверные гарантии бесполезны, если повтор клиента создаёт второе задание. Для этого и нужен обязательный заголовок Idempotency-Key:
- Тот же ключ + то же тело → возвращается исходное задание (200, а не 202). Нового списания не будет никогда.
- Тот же ключ + другое тело →
422 idempotency_key_mismatch: молча вернуть старое задание значило бы скрыть баг в вашем коде. - Ключи хранятся достаточно долго, чтобы покрыть любой реалистичный интервал повторов.
Практическое правило: генерируйте ключ там, где рождается намерение пользователя (один ключ на «пользователь нажал сгенерировать»), а не на каждую HTTP-попытку. После этого HTTP-слой может повторять сколь угодно агрессивно.
намерение → ключ K
попытка 1 (K) — таймаут сети, ответа вы не видели
попытка 2 (K) — 200, возвращается задание из попытки 1Что можно удалить из вашего кода
- Джобы сверки вашей БД с нашей: благодаря леджеру возвратов равенство
sum(списаний) == sum(доставленных изображений) × ценавыполняется по построению. - Алёрты «не списал ли повтор дважды?» — повтор с тем же ключом не может.
- Защитные проверки баланса перед каждой отправкой:
402 insufficient_creditsбезопасно обрабатывать по факту; частичных списаний не бывает.
Что всё же обрабатывать
- Задания
failed: кредиты возвращены, но в UX стоит показатьerror.code(например,content_policy). - 429: бэкофф по
Retry-After; за отклонённые запросы резерв вообще не создаётся. - Истечение подписанных ссылок (15 мин): повторный GET задания даёт свежие — бесплатно.
Полное поведение описано в справочнике API. А если сравниваете провайдеров — спросите каждого, что происходит с вашими деньгами, когда задание умирает между их очередью и их моделью. Именно этот вопрос отличает метрируемые API, которым можно доверять, от тех, которые приходится ежемесячно сверять.