The post has been translated automatically. Original language: Russian
Almost every developer has at least once seen two identical records in the database, created with a difference of half a second. The user clicked the button twice, the mobile client did not wait for a response and sent the request again, the queue delivered the message again. At the same time, the application logic was working normally: it received two correct requests and executed both honestly.
This is solved by idempotence. An idempotent operation, when repeated with the same input data, gives the same result and does not change the state of the system a second time. GET, PUT, and DELETE are idempotent in nature. There is no POST, and that is why the problem almost always arises when creating entities: order, payment, request, registration.
How it is implemented in practice.
- The client generates a unique transaction key and transmits it in the header, usually an Idempotency-Key. The key is created once per user action, rather than per network attempt.
- Prior to the main logic, the server tries to write this key to a separate table with a unique index. If the recording is successful, the operation is performed for the first time.
- If the key is already there, the server does not perform the logic again, but returns the saved result of the first attempt: the same response code and the same identifier of the created entity.
- The key and the result are stored for a limited time, usually from a day to a week, depending on how long the client can repeat the request.
A few details that are often overlooked.
The key must be linked to the user or client, otherwise someone else's key may return someone else's data. The key record and the business operation must be included in the same transaction, otherwise there may be a situation where the key is saved but the entity is not. While the first request is still being executed, it is better not to keep the second one waiting indefinitely: it is more correct to return the 409 and let the client try again later. And it's worth deciding separately what to do if another request body has arrived using the same key. This is usually considered a client error and an explicit rejection is returned, rather than silently applying new data.
Idempotence is needed not only at the entrance to the API. The same principles apply to queue handlers, where message re-delivery is the normal behavior of the broker, not an exception. The rule of thumb is simple: if an operation changes state and the outside world can trigger it, consider that it will be triggered twice.
This is a cheap thing to implement, which saves a lot of time on parsing strange data in production.
Почти каждый разработчик хотя бы раз видел в базе две одинаковые записи, созданные с разницей в полсекунды. Пользователь нажал кнопку дважды, мобильный клиент не дождался ответа и отправил запрос заново, очередь доставила сообщение повторно. Логика приложения при этом работала штатно: она получила два корректных запроса и честно выполнила оба.
Решается это идемпотентностью. Идемпотентная операция при повторном выполнении с теми же входными данными дает тот же результат и не меняет состояние системы второй раз. GET, PUT и DELETE идемпотентны по своей природе. POST нет, и именно поэтому проблема почти всегда возникает на создании сущностей: заказ, платеж, заявка, регистрация.
Как это реализуется на практике.
- Клиент генерирует уникальный ключ операции и передает его в заголовке, обычно Idempotency-Key. Ключ создается один раз на действие пользователя, а не на каждую сетевую попытку.
- Сервер до основной логики пытается записать этот ключ в отдельную таблицу с уникальным индексом. Если запись прошла, операция выполняется впервые.
- Если ключ уже есть, сервер не выполняет логику заново, а возвращает сохраненный результат первой попытки: тот же код ответа и тот же идентификатор созданной сущности.
- Ключ и результат хранятся ограниченное время, обычно от суток до недели, в зависимости от того, как долго клиент может повторять запрос.
Несколько деталей, о которых чаще всего забывают.
Ключ должен быть привязан к пользователю или клиенту, иначе чужой ключ может вернуть чужие данные. Запись ключа и бизнес операция должны попадать в одну транзакцию, иначе возможна ситуация, когда ключ сохранен, а сущность нет. Пока первый запрос еще выполняется, повторный лучше не заставлять ждать бесконечно: корректнее вернуть 409 и дать клиенту повторить попытку позже. И отдельно стоит решить, что делать, если по тому же ключу пришло другое тело запроса. Обычно это считают ошибкой клиента и возвращают явный отказ, а не молча применяют новые данные.
Идемпотентность нужна не только на входе в API. Те же принципы применимы к обработчикам очередей, где повторная доставка сообщения это нормальное поведение брокера, а не исключение. Практическое правило простое: если операция меняет состояние и ее может вызвать внешний мир, считайте, что она будет вызвана дважды.
Это дешевая в реализации вещь, которая экономит много времени на разборе странных данных в продакшене.