Что такое идемпотентность
Что проверяют
Понимаете ли вы, что в распределённой системе повтор запроса это норма жизни, а не редкая авария. Клиент отвалился по таймауту и отправил заново, балансировщик перекинул запрос на другой экземпляр, брокер доставил сообщение второй раз: во всех трёх случаях сервис обязан отработать корректно. Для бэкендера на Go вопрос почти всегда сводится к повторам в HTTP и gRPC клиентах и к тому, чем защищена запись.
Как отвечать
Операция идемпотентна, если состояние системы после нескольких одинаковых вызовов такое же, как после одного. Ответ при этом может отличаться: первый DELETE вернёт успех, второй скажет, что объекта уже нет, и это по-прежнему идемпотентность.
Отделите идемпотентность от безопасности. GET и HEAD безопасны, они вообще ничего не меняют. PUT и DELETE состояние меняют, но повтор ничего не добавляет. POST не идемпотентен по умолчанию, и именно его приходится чинить руками.
Механизм: клиент передаёт ключ операции, сервер одной транзакцией вставляет его в таблицу с уникальным индексом и выполняет работу. Конфликт уникальности означает «уже делали»: возвращаем сохранённый ответ, ничего не повторяя.
// уникальный индекс по key: повторная вставка вернёт конфликт
_, err := tx.ExecContext(ctx, insertKeySQL, key)
if isUniqueViolation(err) {
return loadStoredResponse(ctx, tx, key)
}Где спотыкаются
Путают идемпотентность с безопасностью и заявляют, что идемпотентный запрос ничего не меняет. Меняет, просто повторный вызов не добавляет нового эффекта.
Считают, что раз метод идемпотентен по спецификации, то такова и реализация. PUT, который дописывает элемент в коллекцию, идемпотентность нарушает; гарантию даёт код, а не глагол в заголовке запроса.
Проверяют наличие ключа отдельным запросом перед вставкой. Два параллельных повтора проходят проверку одновременно и создают два заказа: атомарность даёт уникальный индекс или блокировка строки, а не последовательные чтение и запись. Заодно забывают про срок жизни ключей, и таблица растёт бесконечно.