Idempotency: nonce + enforce_nonce

Idempotency: nonce + enforce_nonce

Проблема идемпотентности и дедупликации

При ретраях (таймауты, мобильная сеть, двойной клик) клиент может отправить одно и то же сообщение несколько раз, и сервер создаст дубли.

Как в Discord

Discord поддерживает nonce (до 25 символов) и флаг enforce_nonce: если enforce_nonce=true и nonce присутствует, то nonce проверяется на уникальность “в пределах нескольких минут”; при повторе возвращается уже созданное сообщение и новое не создаётся.

Контракт

Клиент присылает:

  • nonce: str|int (<= 25 chars)

  • enforce_nonce: bool

Поведение:

  • enforce_nonce=false -> nonce не участвует в дедупликации.

  • enforce_nonce=true + nonce -> дедупликация в коротком TTL-окне.

Реализация (идея)

Дедупликацию делаем не постоянной через уникальный индекс в Postgres, а “на 300 секунд” через Redis TTL.

Ключ: nonce:{author}:{nonce} -> значение message_id, TTL = через 300 секунд.

Алгоритм (концептуально):

  1. При enforce_nonce=true выполняем атомарный SET key value NX EX ttl.

  2. Если SET успешен -> создаём сообщение в Postgres и сохраняем message_id в ключ (либо сразу пишем PENDING, затем обновляем на message_id).

  3. Если SET неуспешен -> читаем message_id из Redis и возвращаем уже созданное сообщение.

Почему решение именно такое?

1) Семантика “на несколько минут”, а не навсегда

Мы сознательно не делаем дедупликацию через уникальный индекс в Postgres, потому что это даёт вечную идемпотентность: один и тот же ключ никогда больше не сможет создать новое сообщение. В Discord же enforce_nonce означает проверку уникальности в пределах последних нескольких минут, и при совпадении возвращается уже созданное сообщение. Подобные проблемы возникает только в рамках временных сбоев, поэтому решение в рамка 300 секунд валидно.

2) Атомарность и защита от гонок

Схема “at first SELECT/GET, then INSERT/SET” ломается при конкурентных запросах: два параллельных запроса могут одновременно увидеть отсуствие ключа и оба создать дубль. Команда Redis SET key value NX EX ttl атомарна: ровно один запрос “заберёт” nonce-ключ, а остальные сразу поймут, что это повтор в окне TTL.

3) Производительность под нагрузкой

Проверка/установка ключа в Redis — это один быстрый сетевой round-trip и операция в памяти, поэтому dedup работает быстро даже при высоком QPS. Если бы мы полагались только на Postgres (unique + обработка конфликтов), мы бы чаще упирались в запись/индексы и повышали нагрузку на базу при массовых ретраях.

4) Контроль роста данных

TTL автоматически удаляет idempotency-ключи, поэтому они не накапливаются бесконечно и не требуют фоновой уборки. Это ровно то, что нужно для retry-окон: хранить “память о запросе” ограниченное время.

5) Единое поведение HTTP и WebSocket

Если HTTP и WS создают сообщения разными путями, WS может обходить дедупликацию и снова порождать дубли. Поэтому оба транспорта должны вызывать одну функцию “создать сообщение с учётом nonce/enforce”.

6) Почему Redis?

Redis даёт ограниченное по времени окно дедупликации, а не “вечную” идемпотентность.

Последнее обновление