Что такое идемпотентные запросы

Как REST-архитектура влияет на скорость и надежность работы сайта

В основе REST-архитектуры лежит несколько важных базовых принципов, которые часто упускаются из вида начинающими программистами. Между тем, эти принципы имеют критическое значение для скорости и надежности работы веб-сайта. В некотором смысле REST — это архитектура, концентрирующаяся на совместимости и эффективном взаимодействии с другими узлами сети и клиентским ПО. Для них веб-сайт — черный ящик, реализующий HTTP интерфейс.

Унифицированный программный интерфейс

Ключевой момент: совместимость с HTTP-методами в плане безопасности и идемпотентности.

Безопасный запрос — это запрос, который не меняет состояние приложения.

Идемпотентный запрос — это запрос, эффект которого от многократного выполнения равен эффекту от однократного выполнения.

Таблица соответствия HTTP-метода безопасности и идемпотентности:

HTTP-МетодБезопасныйИдемпотентый
GETДаДа
HEADДаДа
OPTIONSДаДа
PUTНетДа
DELETEНетДа
POSTНетНет

Что мы видим? GET-запрос не должен менять состояние ресурса, к которому применяется. PUT и DELETE запросы могут менять состояние ресурса, но их можно спокойно повторять, если нет уверенности, что предыдущий запрос выполнился. В принципе, это логично: если многократно повторять запрос удаления или замены определенного ресурса, то результатом будет удаление или замена ресурса. Но POST запрос, как мы видим из таблицы, небезопасный и неидемпотентный. То есть мало того, что он меняет состояние ресурса, так и многократное его повторение будет иметь эффект, зависимый от количества повторений. Ему по смыслу соответствует операция добавления новых элементов в коллекцию: выполнили запрос Х раз, и в коллекцию добавилось Х элементов.

Опираясь на понятия безопасности и идемпотентности легче понять, какие именно методы соответствуют операциям в терминах CRUD:

HTTP-методОперация
POSTCreate
GETRead
PUTUpdate
DELETEDelete

Что из этого следует? При проектировании REST интерфейса надо в первую очередь думать, не о том, как будет выглядеть структура URL, а соответствует ли суть выполняемых операций безопасности и идемпотентности выбранного HTTP метода.

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

Отказ хранить состояние клиента и кэширование

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

Более того, внутренняя логика усложняется: приходится использовать сессии. При каждой генерации странички приходится инициировать загрузку сессионных данных из хранилища, сохранение сессионных данных в хранилище. Все это выливается в активное чтение и запись на диск или в БД. Часто веб-сайт спроектирован таким образом, что при посещении поисковым роботом, он все равно стартует для него сессию, хотя смысла в этом нет никакого.

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

Это позволяет закэшировать большинство страниц полностью на продолжительное время, опираясь на возможности HTTP-протокола. В подавляющем большинстве случаев можно обойтись без сложной внутренней логики с кэшированием отдельных SQL-запросов или отдельных блоков страницы через memcache.

REST предлагает нам использовать HTTP-заголовки для управления кэшированием. Это позволяет перенести бремя хранения кэша на клиента, на промежуточные узлы между сервером и клиентом или на специализированное ПО, например, Squid.

Не совсем очевидно, но кэшировать можно не только успешные HTTP-ответы (200 OK), но и другие:

Источник

Почему важна идемпотентность и как писать идемпотентные bash-скрипты

Что такое идемпотентные запросы. Смотреть фото Что такое идемпотентные запросы. Смотреть картинку Что такое идемпотентные запросы. Картинка про Что такое идемпотентные запросы. Фото Что такое идемпотентные запросы

Идемпотентность помогает проектировать более надёжные системы. Это математическая концепция, которую должен понимать каждый разработчик. Операция считается идемпотентной, если её многократное выполнение приводит к тому же результату, что и однократное выполнение. Например, умножение на 1 — идемпотентная операция.

Умножение на ноль — тоже идемпотентная операция.

Ключевая концепция, которую нужно запомнить: однократное выполнение операции может иметь побочные эффекты, но повторное выполнение этой же операции не приведёт ни к чему другому, кроме того, что было сделано при первом выполнении. То есть присваивание — идемпотентная операция.

Вы можете присваивать x значение 4 сколько угодно раз, но x всё равно будет иметь значение 4. При этом присваивание x значения 4 один раз отличается от нуля.

Идемпотентность HTTP-методов

HTTP-методы могут быть идемпотентными или нет.

DELETE — идемпотентный метод. Вы можете сколько угодно раз использовать DELETE, но результат будет всегда таким же, как после первого выполнения операции. Например, DELETE /users/4/contacts/3 удаляет контакт с ID 3. Если вы выполните эту же операцию ещё раз, ничего не произойдёт, так как контакт уже удалён.

GET — тоже идемпотентный метод. Это даже не просто идемпотентный, но ещё и безопасный метод. А безопасные методы можно сравнить с умножением на единицу. Умножать на 1 можно сколько угодно раз, результат всегда будет одинаковым. GET просто получает ресурс. Например, никогда не стоит использовать нормальные ссылки для удаления ресурсов.

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

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

NB! Правило наименьшего удивления или principle of least astonishment гласит, что результат выполнения операции должен быть очевидным и предсказуемым по названию операции. Полезно ознакомиться со статьями «Именование в программировании» и «Ошибки именования в программировании».

Очереди сообщений

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

Это очень распространённый паттерн, и если вы его ещё не используете, стоит подумать о том, чтобы начать.

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

Здесь в игру вступает идемпотентность.

Убедитесь в идемпотентности задач, которые выполняете через воркер

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

Увидели, что что-то идёт не так? Можно снова вызвать функцию, которая отправит все приглашения на все мероприятия. Снова проблемы, когда половина писем отправлена? Исправьте проблему и вызовите функцию снова. Достаточно просто выяснить, какие письма не были отправлены. В качестве бонуса: вы можете понять, сколько писем в день рассылается, когда шлётся больше всего писем и так далее.

Помните, некоторые очереди не гарантируют отправку одного письма один раз. К ним относится Amazon SQS. Поэтому ваши воркеры должны выполнять только идемпотентные задачи.

SQL-миграции

Когда вы выполняете SQL-миграции, позаботьтесь об их идемпотентности.

Например, вам нужно разделить таблицу пользователя на две. Одна таблица будет для пользователей (users), а вторая для деталей, которые не всегда важны (profiles). Вы помещаете внешний ключ user_id в в таблицу profiles. Получаете миграцию, которая берёт каждый ряд в users (SELECT * FROM users) и вставляет ряд с пользовательскими данными в profiles. Запускаете миграцию, но она крашится через час. Это происходит из-за значений NULL, которые вы не учли. Вы исправляете ошибку, снова запускаете миграцию, но вдруг понимаете, что для некоторых пользователей созданы по два профиля.

Этого можно избежать с помощью идемпотентного решения. Вместо SELECT * FROM users можно выбрать пользователей, у которых ещё нет ряда в profiles. С таким решением миграцию можно запускать сколько угодно раз. В ходе каждого запуска будут обработаны данные тех пользователей, у которых ещё нет профиля. Огромное преимущество этого подхода — вам не нужно останавливать приложение при выполнении миграции. Как только вы готовы развернуть новую версию, в которой используется profiles, можно вызвать функцию, которая обеспечит миграцию новых пользователей. Это не самый лучший пример, так как пользователь во время миграции может изменить какие-то данные в таблице users. Учитывайте этот момент.

Денормализованные данные

У вас есть приложение, в котором у каждого пользователя есть много документов. Пользователи могут искать документы по тегам. Теги приходят из разных источников: названия документов, директорий, имена авторов, собственно теги и так далее. Вы решаете держать все теги для всех документов в таблице tags. Это выглядит так:

Когда вы добавляете автора в документ, в таблицу tags попадают данные. Когда вы удаляете автора, соответствующий тег удаляется из таблицы.

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

Вместо функции add_tag_for_new_author вам нужно изначально сделать функцию update_tags_for_document. Эта функция не просто добавляет тег автора. Она проверяет документ, перестраивает список тегов и убеждается, что в базу данных попала корректная информация. При таком подходе таблица tags обрабатывается корректным способом: с помощью кэша. Вы можете удалить все ряды из таблицы и запустить update_tags_for_document. Требуется 2 секунды, чтобы обновить теги? Пусть этим занимается воркер, добавьте сообщение в очередь.

Как писать идемпотентные bash-скрипты

Иногда случается такое: вы пишете bash-скрипт, запускаете его, но через какое-то время он завершается из-за ошибки. Вы фиксите ошибку в системе и снова запускаете скрипт. Но часть шагов вашего скрипта падает с ошибкой, так как эти шаги уже были выполнены при первом запуске. Чтобы создавать отказоустойчивые системы, нужно писать идемпотентные программы.

Bash-идиомы

Ниже вы найдёте несколько советов и bash-идиом, которые помогут писать идемпотентные скрипты. Вы наверняка используете некоторые из них, не задумываясь о побочных эффектах.

Создание пустого файла

Это простая задача. Команда touch по умолчанию идемпотентная. Вы можете вызывать её много раз без проблем. Повторный вызов не повлияет на контент файла. Но он изменит время модификации файла. Если вы его используете, будьте осторожны.

Создание директории

Никогда не используйте команду mkdir как есть. Применяйте её с флагом -p. Этот флаг гарантирует отсутствие ошибки при запуске mkdir, если директория уже существует.

Создание символической ссылки

Для создания символьных ссылок используется такая команда:

Но эта команда генерирует ошибку, если вы повторно вызовите её с существующей целью. Чтобы сделать команду идемпотентной, добавьте флаг -f.

Флаг -f удаляет целевой путь перед созданием символической ссылки, поэтому команда всегда будет успешной. Если вы ссылаетесь на директорию, нужно добавить флаг -n. В противном случае повторный вызов создаст символическую ссылку внутри директории.

Для безопасности всегда используйте такую команду:

Удаление файла

Следующую ниже команду нежелательно использовать для удаления файлов:

Чтобы команда игнорировала несуществующие файлы, надо использовать флаг -f.

Изменение файла

Иногда необходимо добавить новую строку в файл, например, /etc/fstab. Нужно убедиться, что повторный запуск скрипта не приводит к добавлению строки ещё раз. Представьте, что используете такой скрипт:

Если вы запустите скрипт повторно, получите дублирующуюся запись в /etc/fstab. Один из способов сделать скрипт идемпотентным — проверить существование конкретных плейсхолдеров с помощью grep.

В данном случае -q обозначает тихий режим, а -F — режим фиксированной строки. Grep в фоновом режиме завершится с ошибкой, если /mnt/dev не существует, поэтому echo не вызовется.

Проверка существования переменной, файла или директории

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

Вычисление текста может быть дорогой операцией, поэтому вы не захотите писать его каждый раз, когда вызываете скрипт. Для идемпотентности вы проверяете существование файла с помощью флага -f встроенного свойства test командной оболочки.

В данном случае -f — только пример. Есть много других флагов, в том числе:

Например, если вы хотите установить бинарный файл, но только если его ещё не существует, можно использовать флаг -x таким способом:

Это устанавливает файл op в /usr/local/bin. Повторный запуск скрипта не приведёт к повторной установке бинарного файла. В данном случае есть ещё одно преимущество. Вы можете легко обновить бинарный файл. Для этого достаточно удалить его из системы, обновить OP_VERSION env и повторно запустить скрипт. Список флагов и операторов можно получить с помощью man test.

Форматирование устройства

Для форматирования можно использовать такую команду:

Эта команда печатает атрибуты для заданного блока устройства. Соответственно, предварительное добавление значит продолжение форматирования только при ошибке blkid, которая сообщает, что соответствующий том ещё не отформатирован.

Монтирование устройства

Попытка монтировать том в существующий каталог может выполняться с помощью такой команды:

Но если он уже установлен, возникнет ошибка. Можно проверять вывод команды mount. Но есть лучший вариант. Это использование команды mountpoint.

Эта команда проверяет, является ли файл или каталог точкой монтирования. Флаг -q гарантирует, что она ничего не выводит и завершается в фоновом режиме. Если точка монтирования не существует, команда монтирует устройство.

Завершение

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

Адаптированный перевод статей The Importance of Idempotence by Antoine Leclair и How to write idempotent Bash scripts by Fatih Arslan.

Источник

Что такое идемпотентность в методах HTTP?

5 ответов

Что такое идемпотентность в методах HTTP?

Идемпотентность является свойством методов HTTP.

RFC 7231

Давайте посмотрим на RFC 7231, этот документ определяет семантику и содержание HTTP / Протокол 1.1. Смотрите цитаты ниже (основные моменты мои).

Методы HTTP могут быть безопасными :

И / или идемпотент :

Итак, методы HTTP классифицируются как следующие:

RFC 5789

Идемпотент: GET, OUT : ПОЧЕМУ?

Нет Идемпотентность: УДАЛИТЬ, ПОЧТА : ПОЧЕМУ?

Если запрос дает тот же результат

За точно такой же URL, сработал рекурсивно

POST НЕ идемпотент. GET, PUT, DELETE, HEAD, OPTIONS и TRACE являются идемпотентными.

2> Другой даст вам тот же результат

В моем понимании идемпотентность не имеет ничего общего с результатом (= ответ сервера), но с состоянием сервера после одного или нескольких вызовов.

Допустим, вы хотите удалить ресурс на сервере, вызвав

После каждого запроса состояние сервера одинаково, поэтому идемпотентность выполняется. HTTP / 1.1 ничего не говорит об ответе

Метод запроса считается «идемпотентным», если предполагаемое влияние на сервер нескольких идентичных запросов с помощью этого метода такое же, как влияние для одного такого запроса

Идентичные методы (GET, OPTIONS) ничего не меняют на сервере (кроме добавления записей в журнал). Неидемпотентные (PUT, POST, DELETE) методы изменяют данные, которые используются для наполнения контента на веб-страницах или изменения эффекта в другом месте (например, перемещение крана, перевод средств, отправка электронного письма).

Источник

Стажёр Вася и его истории об идемпотентности API

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

Меня зовут Денис Исаев, и я руковожу одной из бэкенд групп в Яндекс.Такси. Сегодня я поделюсь с читателями Хабра описанием проблем, которые могут возникнуть, если не учитывать идемпотентность распределенных систем в своем проекте. Для этого я выбрал формат вымышленных историй о стажёре Васе, который только-только учится работать с API. Так будет нагляднее и полезнее. Поехали.

Что такое идемпотентные запросы. Смотреть фото Что такое идемпотентные запросы. Смотреть картинку Что такое идемпотентные запросы. Картинка про Что такое идемпотентные запросы. Фото Что такое идемпотентные запросы

Про API

Вася разрабатывал приложение для заказа такси с нуля и получил задачу сделать API для заказа машины. Он сидел днями и ночами и реализовал API вида POST /v1/orders :

Когда надо было сделать API для отдачи активных заказов, Вася задумался: а может ли понадобиться заказывать одновременно несколько машин такси? Менеджеры ответили, что нет, такая возможность не нужна. Тем не менее он сделал API для отдачи списка активных заказов в общем виде GET /v1/orders :

В мобильном приложении программист Федя поддержал серверное API следующим образом:

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

Блокирование кнопки

Что такое идемпотентные запросы. Смотреть фото Что такое идемпотентные запросы. Смотреть картинку Что такое идемпотентные запросы. Картинка про Что такое идемпотентные запросы. Фото Что такое идемпотентные запросы

В 8 утра Васю разбудил звонок от саппорта: двое пользователей пожаловались на то, что к ним приехало две машины вместо одной, и деньги списали за обе машины. Быстро делая кофе, Вася сел за ноутбук, подключился по VPN и начал копать логи, графики и код. По логам Вася обнаружил, что у этих пользователей было по два одинаковых запроса с разницей в несколько секунд. По графикам он увидел: в 7 утра база данных начала тормозить и запросы записи в базу стали работать секундами вместо миллисекунд. К этому моменту причина медленных запросов уже была найдена и устранена, но нет гарантий, что подобное не повторится когда-нибудь. И тут он понял: приложение не блокирует кнопку «заказать такси» после отправки запроса, и, когда, запросы начали тормозить, пользователи стали жать на кнопку еще раз, думая, что первый раз она не нажалась.

Что такое идемпотентные запросы. Смотреть фото Что такое идемпотентные запросы. Смотреть картинку Что такое идемпотентные запросы. Картинка про Что такое идемпотентные запросы. Фото Что такое идемпотентные запросы

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

В подземном переходе

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

Потратив два дня на раскопки этого единичного случая, они выяснили, в чем было дело. Оказалось, что блокировать кнопку недостаточно: один из пользователей пытался заказать такси, находясь в подземном переходе. Мобильный интернет у него работал еле-еле: при нажатии на кнопку заказа запрос ушел на сервер, но ответ не был получен. Приложение показало сообщение «произошла ошибка» и разблокировало кнопку заказа. Кто бы мог подумать, что такой запрос мог быть успешно выполнен на сервере, а таксист уже быть в пути?

Выбрали вариант править на сервере, так как это можно сделать в тот же день, не дожидаясь долгой раскатки приложения. Из нескольких вариантов исправления Вася выбрал такой: перед созданием заказа в базе он селектит из базы заказы пользователя с такими же параметрами from и to за последние 5 минут. Если такой заказ найден, то сервер отдает ошибку 500. Вася написал автотесты, и, случайно, запустил их параллельно: один из тестов упал. Вася понял, что есть гонка между селектом и инсертом в базу при параллельных запросах от одного пользователя. По результатам случившихся багов Вася понял, что и сеть может «моргать», и база данных может тормозить, увеличивая окно гонки, поэтому случай вполне реальный. Как это чинить правильно, было непонятно.

Лимиты на число активных заказов

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

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

Мультизаказ

Прошел месяц, и к Васе пришел новенький менеджер: за сколько дней можно сделать фичу «мультизаказ»: чтобы пользователь мог заказать две машины такси? Вася удивлен: как же так, я же спрашивал, и вы говорили мне, что это не понадобится?! Вася сказал, что это не быстро. Менеджер удивился: разве это не просто поднять лимит с 1 до 2? Но мультизаказ полностью ломал Васину схему защиты от дублей. Вася даже не представлял, как вообще можно решить эту задачу, не вводя дублей.

Что такое идемпотентные запросы. Смотреть фото Что такое идемпотентные запросы. Смотреть картинку Что такое идемпотентные запросы. Картинка про Что такое идемпотентные запросы. Фото Что такое идемпотентные запросы

Ключ идемпотентности

Вася решил изучить, кто как борется с такими проблемами, и наткнулся на понятие идемпотентности. Идемпотентным называют такой метод API, повторный вызов которого не меняет состояние. Здесь есть тонкий момент: результат идемпотентного вызова может меняться. Например, при повторном вызове идемпотентного API создания заказа — заказ не будет создаваться еще раз, но API может ответить как 200, так и 400. При обоих кодах ответа API будет идемпотентно с точки зрения состояния сервера (заказ один, с ним ничего не происходит), а с точки зрения клиента поведение существенно разное.

Также Вася узнал, что HTTP методы GET, PUT, DELETE формально считаются идемпотентными, тогда как POST и PATCH нет. Это не означает, что вы не можете сделать GET неидемпотентным, а POST идемпотентным. Но это то, на что полагается множество программ, например, прокси-серверы могут не повторять POST и PATCH запросы при ошибках, тогда как GET и PUT могут повторить.

Вася решил посмотреть примеры и наткнулся на понятие idempotency key в некоторых публичных API.

Яндекс.Касса позволяет клиентам слать вместе с формально неидемпотентными (POST) запросами заголовок Idempotency-Key с уникальным ключом, сгенерированным на клиенте API. Рекомендуется использовать UUID V4. Stripe аналогично позволяет клиентам слать вместе с формально неидемпотентными (POST) запросами заголовок Idempotency-Key с уникальным ключом, сгенерированным на клиенте API. Ключи хранятся в течение 24ч. Среди неплатежных систем Вася нашел client tokens у AWS.

Вася добавил в запрос POST /v1/orders новое обязательное поле idempotency_key, и запрос стал таким:

Приложение стало генерировать ключ идемпотентности как UUID v4 и слать его на сервер. При повторных попытках создания заказ приложение шлет тот же ключ идемпотентности. На сервере ключ идемпотентности инсертится в базу в поле, на котором есть ограничение базы данных по уникальности. Если это ограничение не дало сделать инсерт, то код обнаруживал это и отдавал ошибку 409. По совету Феди этот момент был переделан в сторону упрощения приложения: отдавать стали не 409, а 200, будто бы заказ успешно создан, тогда на клиентах не надо учиться обрабатывать код 409.

Что такое идемпотентные запросы. Смотреть фото Что такое идемпотентные запросы. Смотреть картинку Что такое идемпотентные запросы. Картинка про Что такое идемпотентные запросы. Фото Что такое идемпотентные запросы

Баг при тестировании

После этого лимит просто подняли с 1 до 2 и поддержали изменение в приложении. При тестировании приложения нашли следующий баг:

Сначала Вася предложил Феде генерировать новый ключ идемпотентности в таком случае. Но Федя объяснил, что тогда может быть дубль: при сетевой ошибке запроса создания заказа приложение не может знать, был ли действительно заказ создан.

Федя заметил, что хоть это и не решение, но для раннего обнаружения таких багов на сервере следовало проверять, что параметры входящего запроса совпадают с параметрами существующего заказа с таким же ключом идемпотентности. Например, AWS отдает ошибку IdempotentParameterMismatch в таком случае.

В итоге вдвоем они придумали следующее решение: приложение не дает изменить параметры заказа и бесконечно пытается создать заказ, пока получает коды ответа 5xx или же сетевые ошибки. Вася добавил серверную валидацию, предложенную Федей.

Полезное код-ревью

На код-ревью реализованного решения нашли два проблемных сценария.

Сценарий 1: два такси

Сценарий 2: приехало отмененное такси

Вася с Федей рассматривали простые варианты, как поправить обе проблемы:

Но старшие товарищи предложили более общее решение: версионировать состояние списка заказов. API GET /v1/orders отдавало бы версию списка заказов. Это версия всего списка заказов пользователя, а не конкретного заказа. При создании заказа приложение передает в отдельном поле или заголовке If-Match версию, о которой он знает. Сервер атомарно с изменением увеличивает версию при любых изменениях заказов (создание, отмена, редактирование). То есть приложение в запросе к серверу говорит ему, какое состояние заказов оно знает. И если это состояние заказов (версия) расходится с тем, что хранится на сервере, то сервер отдает ошибку «заказы были изменены параллельно, перезагрузите информацию о заказах». Версионирование решает обе найденные проблемы, и именно его Вася с Федей и поддержали. Также стоит отметить, что версия может быть как числом (номером последнего изменения), так и хэшом от списка заказов: так, например, работает параметр fingerprint в Google Cloud API для изменения тегов инстансов.

Время делать выводы

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

Идемпотентность удаления

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

Отмена заказа делалась через запрос DELETE /v1/orders/:id. Внутри строка с заказом просто удалялась. В soft delete (выставление deleted_at=now()) необходимости не было.

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

Оказывается, идемпотентным должны быть не только API создания, но и удаления ресурсов, — подумал Вася.

Вася рассматривал вариант отдавать 200 всегда, даже если DELETE запрос в базе не удалил ничего. Но это создавало риск скрыть и пропустить возможные проблемы. Поэтому он решил сделать soft delete и переделать API отмены:

Больше подобных проблем с отменой заказа не всплывало.

Идемпотентность изменения

Изменение точки B

Вася решил проверить по коду, есть ли идемпотентность у API изменения поездки: он уже осознал, что идемпотентным должен быть абсолютно любой API.

Что такое идемпотентные запросы. Смотреть фото Что такое идемпотентные запросы. Смотреть картинку Что такое идемпотентные запросы. Картинка про Что такое идемпотентные запросы. Фото Что такое идемпотентные запросы

В приложении пассажир может изменить точку B. При этом посылается запрос PATCH /v1/orders/:id :

Сервер же внутри просто выполняет update в базу:

Тут все идемпотентнее некуда — подумал Вася и был прав. Только не учел он того, что при параллельном изменении и чтении/изменении тут могут быть гонки, но это уже совсем другая история.

А надо ли фиксить

Также Вася проверил API завершения поездки: оно вызывается водительским приложением, когда водитель выполнил заказ. На сервере API помечает заказ выполненным и делает ряд действий, в том числе подсчет статистики. Среди считаемой статистики взгляд Васи упал на метрику кол-ва завершенных заказов у пользователя. При вызове API счетчик завершенных заказов инкрементился запросом вида

Стало понятно, что при повторных вызовах API счетчик может увеличиться больше, чем на 1.

Что такое идемпотентные запросы. Смотреть фото Что такое идемпотентные запросы. Смотреть картинку Что такое идемпотентные запросы. Картинка про Что такое идемпотентные запросы. Фото Что такое идемпотентные запросы

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

Вася создал задачу в таск-трекере на переделку расчета счетчика по следующему алгоритму:

Через полчаса Васю спросил его руководитель: зачем это делать? После небольшого обсуждения у них появилось взаимное понимание, что редкое расхождение счетчиков приемлемо. И переделывать схему для точного подсчета метрики нецелесообразно для бизнеса на данном этапе.

Все проверил

Как ответственный стажер-разработчик, Вася проверил все места, где API может быть неидемпотентно. Но точно ли он проверил все, что нужно?

Идемпотентность при внешних операциях

Дубли SMS

В середине рабочего дня к столу Васи прибегает обеспокоенный менеджер: в фейсбуке медийная личность написала гневный пост о том, что наше такси-приложение завалило его десятком одинаковых SMS. Реагировать нужно немедленно, пост уже собрал сотни лайков.

Что такое идемпотентные запросы. Смотреть фото Что такое идемпотентные запросы. Смотреть картинку Что такое идемпотентные запросы. Картинка про Что такое идемпотентные запросы. Фото Что такое идемпотентные запросы

Вася внимательно просмотрел код отправки SMS: сначала в очередь клалась задача, затем при исполнении задачи делался запрос в SMS шлюз. Ни там, ни там не было перезапросов в случае ошибок. Откуда же могли взяться дубли, может быть проблема у шлюза или оператора? Затем Вася обнаружил, что во время дублей consumer очереди многократно крэшился. Его осенило: задача берется из очереди, выполняется, и помечается выполненной только в конце исполнения.

На исправление понадобилось два дня: для задач, отправляющих SMS, email и пуши, изменилась логика пометки задачи выполненной: пометка стала делаться в самом начале выполнения. В терминах распределенных систем, Вася перешел от «at least once delivery» к «at most once delivery». Были настроены мониторинги, продуктово было согласовано, что недоставка нотификаций лучше, чем их дублирование.

Заключение

С помощью выдуманных историй я попытался объяснить, почему так важно, чтобы API были идемпотентными. Показал, какие есть нюансы на практике.

В Яндекс.Такси мы всегда думаем об идемпотентности наших API. В небольшом проекте допустимо было бы не тратить время на проработку редких случаев. Но Яндекс.Такси это десятки миллионов поездок ежемесячно. Поэтому у нас есть процедура дизайн-ревью архитектуры и API. Если что-то неидемпотентно, есть гонки, либо логические проблемы, то API не пройдет ревью. Для разработчиков это означает, что приходится внимательно относиться к деталям и продумывать множество граничных случаев. Это нетривиальная задача, и особенно сложно покрывать такие граничные случаи автотестами.

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

Источник

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *