Idempotency key в публикации: как не создать два одинаковых поста

Повторный запуск публикации — нормальная часть рабочего процесса. Скрипт упал после отправки запроса, очередь переиграла задачу, оператор нажал кнопку дважды, CI прогнал шаг повторно. Если публикационный контур не умеет отличать «попытку» от «новой команды», система начинает дублировать посты, карточки, уведомления и записи в журнале.

Idempotency key решает именно эту задачу: делает повторный запрос безопасным. Ниже — практический способ встроить этот механизм в публикационный workflow так, чтобы одна и та же публикация не создавалась дважды, даже если запуск повторяется.

Что именно нужно защитить

В публикации обычно есть два разных типа действий:

  1. Создание нового объекта — пост, карточка, анонс, запись в ленте.
  2. Повторная попытка уже начатого действия — тот же запрос, но из-за сбоя, таймаута или ручного повтора.

Проблема начинается тогда, когда система не видит разницы между ними. С точки зрения API это выглядят как одинаковые вызовы «создай пост». Если первый вызов уже прошёл на стороне сервера, а ответ не дошёл до клиента, второй запуск может создать второй такой же пост.

Idempotency key — это не «антидубль вообще», а метка одной бизнес-операции. Если операция одна и та же, ключ остаётся тем же. Если задача новая — ключ новый.

Важно держать в голове простое правило:
идемпотентность нужна не для текста поста, а для конкретной попытки публикации.

Как работает idempotency key в публикационном сценарии

Рабочая схема обычно выглядит так:

  1. Система формирует команду на публикацию.
  2. Вместе с командой создаётся idempotency key.
  3. Этот ключ отправляется в сервис публикации.
  4. Сервис проверяет, видел ли он такой ключ раньше.
  5. Если ключ новый — создаёт пост и сохраняет результат вместе с ключом.
  6. Если ключ уже был — не создаёт новый пост, а возвращает ранее зафиксированный результат.

То есть ключ связывает все повторы одной и той же операции в одну запись.

На практике это полезно в таких ситуациях:

  • таймаут при ответе от API;
  • повторный запуск job после ошибки;
  • ручной retry в админке;
  • восстановление очереди сообщений;
  • дублирование события на уровне интеграции;
  • повторная отправка из-за сетевого сбоя.

Здесь важно различать идемпотентную публикацию и неизменяемую публикацию. Идемпотентность не запрещает редактирование поста позже. Она лишь фиксирует, что один и тот же запуск не должен создавать второй объект.

Из чего должен состоять ключ

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

Практически ключ обычно строят из таких компонентов:

  • идентификатор канала или площадки;
  • тип действия: publish, schedule, republish;
  • внутренний ID черновика или сообщения;
  • версия контента, если одна публикация может существовать в нескольких редакциях;
  • иногда — временное окно, если операция относится к расписанию.

Пример логики без привязки к конкретной технологии:

  • channel:telegram
  • action:publish
  • draft:48291
  • version:7

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

Что делать нельзя

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

Где хранить ключ и как долго помнить операцию

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

Хранить стоит минимум такие данные:

  • сам idempotency key;
  • статус операции: начата, успешно завершена, завершена с ошибкой;
  • созданный объект или его ID;
  • время первой обработки;
  • срок жизни записи.

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

Ниже компактная таблица для выбора подхода.

Подход Когда подходит Риск дублей Что помнить
Ключ только в памяти процесса Очень короткие локальные операции Высокий Сбой процесса стирает историю
Ключ в кэше с TTL Частые повторы в коротком окне Средний TTL должен покрывать все ретраи
Ключ в постоянном хранилище Публикации с важной атомарностью Низкий Нужен контроль жизненного цикла записей

Если публикация критична для бизнеса, полагаться только на память процесса не стоит. После рестарта вы теряете защиту именно там, где она нужна больше всего.

Как встроить ключ в публикационный workflow

Надёжный workflow строится не вокруг одного запроса, а вокруг всей цепочки: подготовка → проверка → отправка → фиксация результата → повтор.

1. Сформировать команду публикации до отправки

Ключ должен появляться на этапе подготовки задачи, а не после неудачи. Иначе один и тот же retry может получить новый ключ и превратиться в новый пост.

Практический принцип:
одна бизнес-команда — один ключ — все ретраи с ним же.

2. Передать ключ во все участки цепочки

Если публикация проходит через очередь, воркер и внешний API, ключ должен идти сквозь всю цепочку. Иначе один сервис решит, что операция уникальна, а следующий уже потеряет контекст.

3. Фиксировать результат атомарно

При успешной публикации нужно сохранить связку:

  • ключ;
  • ID созданного поста;
  • статус успешного завершения.

Если результат не зафиксирован, система может повторно отправить запрос после сбоя и создать дубль.

4. Для повторного запроса возвращать тот же результат

Если ключ уже обработан, лучше вернуть не новый объект, а ранее созданный ID или уже известный статус. Для клиента это означает: «операция завершена, повтор не меняет результат».

5. Отличать «повтор» от «новой версии»

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

  • повтором с тем же ключом;
  • новой публикацией с новым ключом.

Типичные ошибки в реализации

Ниже — ошибки, которые чаще всего ломают защиту от дублей.

1. Генерация нового ключа на каждом retry

Если retry получает новый ключ, система перестаёт понимать, что это та же операция. В результате вместо защиты от дубля получается его ускоритель.

2. Ключ не связан с бизнес-сущностью

Когда ключ относится только к HTTP-запросу, а не к черновику, версии и каналу, защита работает случайно. Любое изменение маршрута или транспорта сбивает идентификацию.

3. Нет сохранения результата

Ключ есть, но вместе с ним не хранится ID созданного поста. Тогда при повторе сервис не может ответить корректно и начинает создавать объект заново.

4. Слишком короткий срок хранения

Через час защита работает, через сутки — нет. Если ретраи и восстановление очереди живут дольше TTL, дубль неизбежен.

5. Смешение публикации и редактирования

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

Практический чек-лист перед запуском

Вот короткий рабочий список перед тем, как включать публикацию в автоматический контур:

  • [ ] У каждой публикационной команды есть стабильный idempotency key.
  • [ ] Повторный запуск использует тот же ключ, а не новый.
  • [ ] Ключ связан с черновиком, каналом и версией операции.
  • [ ] Сервис хранит статус и ID созданного поста.
  • [ ] Срок хранения ключа покрывает все вероятные retry-сценарии.
  • [ ] Есть явное различение между повтором и новой редакцией.
  • [ ] При повторе система возвращает уже известный результат, а не создаёт новый объект.

Если хотя бы один пункт не выполнен, защита от дублей неполная.

Критерий готовности: что должно происходить при сбое

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

Дальше смотрите на поведение системы:

  • при повторе та же команда не создаёт новый пост;
  • сервис возвращает уже созданный ID;
  • журнал показывает одну бизнес-операцию, а не две;
  • пользователь или оператор видит понятный статус, а не хаос из дублей.

Если после сбоя система не может уверенно сказать, что операция уже была, значит, idempotency key внедрён формально, но не как часть процесса.

Вывод

Idempotency key в публикации — это не дополнительная сложность, а способ сделать рабочий повтор безопасным. Он нужен там, где один и тот же запуск может быть отправлен дважды: из-за сбоя, очереди, таймаута или ручного ретрая.

Рабочая логика простая:
одна бизнес-операция получает один ключ, ключ живёт дольше всех retry-сценариев, а повтор возвращает прежний результат, а не создаёт новый пост.

Если так устроить публикационный workflow, повторный запуск перестаёт быть угрозой. Он становится обычной частью надёжной операции.