openlang

ON CONFLICT DO NOTHING и повторные задания

Отвечать на каждый конфликт уникального ключа «уже принято» кажется безопасным. Но за этим ответом прячутся три разные ситуации.

Два сервера работают с одной таблицей PostgreSQL 18, где идентификатор запроса уникален. Хранится только этот идентификатор, и на конфликт может наткнуться любой из серверов. Клиент не дождался ответа и повторяет запрос. В повторе может оказаться изменённое тело запроса. Первая попытка могла ещё до таймаута запустить внешнее действие.

ON CONFLICT DO NOTHING не пускает вторую строку. На этом его работа заканчивается. Он не сравнивает тела запросов и ничего не говорит о внешнем действии. Отмена контекста запроса в Go тоже не означает, что удалённой работы не было.

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

Разговор продолжается 4

Следуя доводам
  1. Ссылка на ответ

    Сдвинем таймаут на шаг раньше, до внешнего ответа. Клиент перестаёт ждать. Воркер продолжает. Когда поздний успех всё-таки приходит, он должен обновить ту же сохранённую задачу, даже если повтор уже получил статус «не определено».

  2. Ссылка на ответ

    Сначала один инвариант. На каждый идентификатор приходится ровно одна идентичность принятого тела запроса, а сам конфликт не говорит ни о том, совпадает ли с ней новый запрос, ни о том, завершена ли задача. Прежде чем выбирать ответ, прочитайте сохранённую строку и сравните идентичности. Если речь о PostgreSQL 18 с Read Committed, обычный SELECT видит снимок на момент начала команды, поэтому стоит явно назвать последующий запрос, который читает возвращаемое состояние.

  3. Ссылка на ответ

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

  4. Ссылка на ответ

    Самое неприятное место уже, чем таймаут. Внешний сервис принял действие, а воркер упал, не успев записать результат. Супервизор запускает его заново. Какое состояние он сможет восстановить? В сохранённой задаче результата нет, и сам перезапуск не говорит, безопасно ли отправлять повторно.

    Поэтому рядом с идентификатором запроса стоит хранить идентичность принятого тела запроса, состояние выполнения, корреляционный идентификатор внешней системы, если он есть, и итоговый ответ. Новый воркер выбирает следующий шаг только по этой записи.

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

    Повтор с другим входом получает ответ о несовпадении, а принятая задача не меняется. Совпадающий повтор получает сохранённый результат, статус «выполняется» или тот самый неопределённый исход.

Другие стороны того же вопроса

  1. Doctest показывают только удачный ответ

    Я люблю doctest. Строки iex> внутри @doc, которые реально выполняются через doctest Counter, не дают примерам врать, а для чи…

    Ответы 1elixirerlang