что такое идемпотентный метод в rest api
Что такое Идемпотентность?
Идемпотентность
С точки зрения RESTful-сервиса, операция (или вызов сервиса) идемпотентна тогда, когда клиенты могут делать один и тот же вызов неоднократно при одном и том же результате, работая как «сеттер» в языке программирования. Другими словами, создание большого количества идентичных запросов имеет такой же эффект, как и один запрос. Заметьте, что в то время, как идемпотентные операции производят один и тот же результат на сервере (побочные эффекты), ответ сам по себе может не быть тем же самым (например, состояние ресурса может измениться между запросами).
Методы PUT и DELETE по определению идемпотентны. Тем не менее есть один нюанс с методом DELETE. Проблема в том, что успешный DELETE-запрос возвращает статус 200 (OK) или 204 (No Content), но для последующих запросов будет всё время возвращать 404 (Not Found), если только сервис не сконфигурирован так, чтобы «помечать» ресурс как удалённый без его фактического удаления. Как бы то ни было, когда сервис на самом деле удаляет ресурс, следующий вызов не найдёт этот ресурс и вернет 404. Состояние на сервере после каждого вызова DELETE то же самое, но ответы разные.
Методы GET, HEAD, OPTIONS и TRACE определены как безопасные, что также делает их идемпотентными. Прочитайте секцию о безопасности ниже.
Безопасность
Некоторые HTTP-методы (например: HEAD, GET, OPTIONS и TRACE) определены как безопасные, это означает, что они предназначены только для получения информации и не должны изменять состояние сервера. Другими словами, они не должны иметь побочных эффектов, за исключением безобидных эффектов, таких как: логирование, кеширование, показ баннерной рекламы или увеличение веб-счетчика. Созданный произвольный GET-запрос, который не учитывает контекст состояния приложения, следует считать безопасным.
Упрощенно, безопасность означает, что вызов метода не имеет побочных эффектов. Следовательно, такие (безопасные) запросы клиенты могут безопасно совершать неоднократно, не опасаясь изменить состояние сервера. Это означает, что сервисы должны придерживаться определения безопасности для GET, HEAD, OPTIONS и TRACE операций. Не выполнения этого свойства может приводить в заблуждение потребителя сервиса, а также вызвать проблемы для веб-кеширования, поисковых систем и других автоматизированных агентов, которые непреднамеренно будут изменять состояние сервера.
По определению, безопасные операции идемпотентны, так как они приводят к одному и тому же результату на сервере.
Безопасные методы реализованы как операции только для чтения. Однако безопасность не означает, что сервер должен возвращать тот же самый результат каждый раз.
Данный сайт является переводом RestApiTutorial.com
©Андрей Куманяев, 2012-2014. Все права защищены.
Qsusha
У REST архитектуры есть 6 важных глаголов GET, POST, PUT, DELETE, PATCH OPTIONS и HEAD.
У них есть ключевые характеристики: безопасность и идемпотентность.
Безопасность — это просто. Если операция может изменить ресурс — она называется небезопасной в рамках REST архитектуры.
GET, OPTIONS, HEAD — работают только на чтение, поэтому они безопасны для ресурса.
POST, PUT, DELETE — могут модифицировать данные, поэтому они небезопасные.
Идемпотентная операция — действие, многократное повторение которого эквивалентно однократному.
PUT, GET, OPTIONS, DELETE, HEAD — идемпотентные.
POST, PATCH — неидемпотентные.
Идемпотентный и небезопасный — значит, что, сколько бы ни повторялся запрос, только первый изменит состояние системы, а остальные состояние системы не меняют.
Идемпотентный и безопасный — многократное повторение запроса вернет одно и то же состояние системы (если ресурс не изменился между ними по иным причинам).
Разница между PUT и POST запросами в первую очередь в том, что PUT идемпотентный, а POST нет.
POST — это создание новой записи, независимо от того, есть аналогичная или нет. Если мы 10 раз пошлем POST по одному и тому же URI — то будет создано 10 идентичных записей.
PUT приравнивают к Update, а POST — create. И это может внести сумятицу.
PUT не только update, эта операция создаст запись, если ее не было. Update — тоже не слишком корректно звучит — PUT делает replace всей записи. Если запись уже имеется — то у PUT нет иного варианта, кроме как обновить ее полностью. PUT — ‘то операция для создания или замены уже имеющегося объекта (Replace/Create) по заданому URI. Важно: PUT обновляет всю запись целиком.
PATCH: Есть случаи, когда необходимо изменить лишь один атрибут объекта в рамках запроса SaveCheckList. Например, флаг isCollection : false на isCollection :true. Если мы используем PUT SaveCheckList, нам необходимо передать на URI все параметры чеклиста (TODO: Уточнить. Уже не уверена, встречала на Stackoverflow иное). Если же мы используем Patch — то по данному URI отправляем только один флаг isCollection :true. И будет обновлена в таком варианте не вся запись, а лишь флаг isCollection.
Но PATCH, в отличие от PUT — неидемпотентен. Это опасно коллизиями множественных PATCH запросов. Нужно уметь отвечать множественным PATCH по одному и тому же URI в рамках одной операции, что, родной, нет, я второй раз обновлять флаг не буду. Т.е. несмотря на то, что сам PATCH неидемпотентен, мы должны реализовать его так, чтоб он работал как идемпотентный. (TODO: не поняла, зачем тогда PATCH такой неидемпотентный-то сделан? какие кейсы для неидемпотентности PATCH актуальны? http://restcookbook.com/HTTP%20Methods/patch/ — тут куча разных объяснений, надо еще покурить).
Если говорить о конкретных примерах:
Допустим, я создаю новый чеклист и каждый раз, когда я жму на кнопку «Сохранить» отправляется запрос на сервер, я (при должной ловкости и тормознутости UI) могу нажать 4 раза на кнопку, если при этом на сервере не валидируется уникальность контента, то получу на выходе:
В случае с POST запросом будет создано 4 идентичные записи.
В случае с PUT — будет создана лишь одна запись.
Если мне нужно обновить чеклист и не нужна история обновлений — я спокойно шлю PUT SaveCheckList со всеми аттрибутами.
Если мне, к примеру, нужно знать, какой пользователь внес изменения в чеклист и когда (т.е. в базе должны храниться состояния) — тогда PUT не подходит — нужен POST.
REST говорит о том, что не нужно плодить лишние сущности. нужно использовать GET, если «мне только посмотреть»
OPTIONS — используется для получения информации о принимаемых типах запросов по URI? актуально для предполетных запросов в случае кросс-доменных запросов CORS. В ответе на OPTIONS, API, к которому мы обращались, вернет все методы, которые позволено применять к запрашиваемому URI:
Access-Control-Allow-Methods: POST, GET, OPTIONS, DELETE
а также скажет, в течение какого срока действует данное «уникальное» предложение:
Борьба с дубликатами: делаем POST идемпотентным
Проблема
Представим, что у вас клиент-серверное приложение, REST API на бекенде и какая-нибудь форма на фронте. И вот наступает момент, когда в баг-трекере появляется запись про «создалось N дубликатов «. Начинаете выяснять, оказывается что на клиенте кнопка сохранения формы не меняла статус на disabled в процессе сохранения, а у клиента ускоренный метаболизм, и он успел много раз по ней нажать, пока интерфейс не сообщил заветное «ок».
Решение
Форма на клиенте могла бы генерировать некоторый ключ на этапе заполнения и затем использовать его в запросе сохранения заказа. Сколько бы раз пользователь не нажал на кнопку, каждый запрос сохранения сопровождался бы этим ключом, запросы с одинаковым ключом сервер бы идентифицировал как дубли.
Может показаться, что это лишь подмена одной проблемы другой: вместо обязательства блокировки кнопки клиент появляется обязательство помечать запросы ключом. Да, но это другое Например, ключ помимо прочего обеспечивает воспроизводимость результата в условиях обрыва соединения. Если пользователь нажал на кнопку оформления заказа, въезжая в тоннель, он не узнает о том, что его заказ был сохранён и наверное увидит в интерфейсе что-то вроде «Нет сети». При должной обработке на клиенте он сможет нажать на кнопку ещё раз, как только вернётся в онлайн, и получит ответ о созданном заказе без создания дубликата.
Немного теории
Выше было описано доступно, но теорию тоже неплохо знать. Если вернуться к ней, типовое назначение методов REST API на HTTP выглядит так (как используете вы, можете писать в комментариях):
Реализация
Почему такой набор? Потому что удобно строить pipeline обработки. А вообще выбрано на вкус автора, можете пофантазировать на свой.
Сетап
Есть контроллер, который умеет создавать заказ и возвращать его по id:
Модели создания и чтения заказа:
Справка. Немного про MediatR и PipelineBehavior (если в курсе, можете смело пропускать)
Помимо прочего, по аналогии с OWIN pipeline здесь есть возможность встроить дополнительное поведение в обработку запроса. Делается это с помощью PipelineBehavior. Обработчики образуют цепочку. Что-то вроде комбинации chain of responsibility + decorator/proxy (разница тут очень тонкая, можете объяснить в комментариях). Запрос передаётся первому обработчику. Затем каждый обработчик в цепочке, имея ссылку на следующий, может как передать обработку дальше по цепочке, так и не делать этого, при этом он имеет контроль над запросом и результатом. Псевдокод:
Делаем POST идемпотентным
Долой текста, вот код:
Используется это затем так:
Код контроллера при этом остаётся без изменений:
2 запроса без Idempotency-Key обрабатываются как разные (всё внимание на id в ответе). Первый:
2 запроса с одинаковым Idempotency-Key возвращают один и тот же объект. Здесь для наглядности показан лог консоли Postman, чтобы было видно, что это два разных запроса. Обратите внимание, что id в ответах не отличаются. Первый:
Подводим итоги
Добавили поддержку идемпотентности для запроса создания ресурса, сохранив обратную совместимость и сделав это без модификации логики приложения. Подключение происходит прозрачно для вызывающего кода. Добавление поддержки для отдельно взятой операции довольно дёшево, требует только константы commandType и пары методов сериализации и десериализации результата выполнения, что ещё и неплохо масштабируется.
Что интересного ещё стоит отметить:
Границы уникальности. Нужно учитывать, что клиент может не обеспечивать глобальной уникальности idempotencyKey. В этом примере я требуют уникальности в контексте отдельной операции, запрошенной пользователем. Здесь проверяется связка userId + commandType + idempotencyKey, таким образом запрос оформления заказа и запрос добавления в избранное будут иметь разные commandType, а запросы от разных пользователей будут помечены разными userId, благодаря чему они не пересекутся.
Абстрагируем способ получения idempotencyKey. Не редко в приложениях помимо внешних запросов встречаются консюмеры Kafka, таски Hangfire и др. механизмы, для которых способ передачи IdempotencyKey может отличаться.
Абстрагируем способ получения userId. Причины аналогичны предыдущему пункту.
Контракт IIdempotencyCommand требует всего 2 вещи:
определить commandType, который будет использоваться для изоляции;
определить способ сериализации и десериализации результата. Опять же, спасибо IoC, проблемы обратной совместимости делегируем конкретным командам.
Мой первый пост на Хабре, буду оч признателен за обратную связь. В общем, like, share, repost.
Путешествие к идемпотентности и временному разделению
Идемпотентность в HTTP означает, что один и тот же запрос может быть выполнен несколько раз с тем же эффектом, как если бы он был выполнен только один раз. Если вы замените текущее состояние какого-либо ресурса новым, независимо от того, сколько раз вы это сделаете, конечное состояние будет таким же, как если бы вы делали это только один раз. Чтобы привести более конкретный пример: удаление пользователя идемпотентно, потому что независимо от того, сколько раз вы удаляете данного пользователя по уникальному идентификатору, в конечном итоге этот пользователь будет удален. С другой стороны, создание нового пользователя не является идемпотентом, потому что запрос такой операции дважды создаст двух пользователей. В терминах HTTP вот что говорит RFC 2616: 9.1.2 Идемпотентные методы :
9.1.2. Идемпотентные методы
Методы также могут иметь свойство « идемпотентности », заключающееся в том, что […] побочные эффекты от N> 0 идентичных запросов такие же, как и для одного запроса. Методы GET, HEAD, PUT и DELETE разделяют это свойство. Кроме того, методы OPTIONS и TRACE НЕ ДОЛЖНЫ иметь побочных эффектов, и поэтому являются по своей сути идемпотентными.
Временная связь является нежелательным свойством системы, где правильное поведение неявно зависит от измерения времени. Проще говоря, это может означать, что, например, система работает только тогда, когда все компоненты присутствуют одновременно. Для блокировки обмена запросом-ответом (ReST, SOAP или любая другая форма RPC) требуется, чтобы и клиент, и сервер были доступны одновременно, что является примером такого эффекта.
Который в свою очередь генерирует запрос, подобный этому:
Во всех этих случаях вы просто получаете исключение на стороне клиента и не знаете, каково состояние сервера. Технически вы должны повторить неудавшиеся запросы, но, поскольку POST не идемпотентен, вы можете в конечном итоге наградить игрока более чем одним мечом (в случаях 5-8). Но без повторов вы можете потерять деньги игрока, не дав ему его драгоценный артефакт. Должен быть лучший способ.
Превращение POST в идемпотентный PUT
API выглядит следующим образом:
Почему это так важно? Проще говоря (клиент не предназначен) теперь может повторять запрос PUT столько раз, сколько он хочет. Когда сервер получает PUT в первый раз, он сохраняет меч в базе данных с UUID, сгенерированным клиентом ( 45e74f80-b2fb-11e4-ab27-0800200c9a66 ) в качестве первичного ключа. В случае второй попытки PUT мы можем либо обновить, либо отклонить такой запрос. С POST это было невозможно, потому что каждый запрос рассматривался как покупка нового меча — теперь мы можем отследить, был ли такой PUT раньше или нет. Мы просто должны помнить, что последующий PUT — это не ошибка, это запрос на обновление:
Примечание 1: Я использую тип UUID в моделях контроллера и JPA. Они не поддерживаются «из коробки», для JPA вам нужен специальный конвертер:
Аналогично для Spring MVC (только в одну сторону):
Примечание 2: если вы не можете изменить клиента, вы можете отслеживать дубликаты, сохраняя хэш каждого запроса на стороне сервера. Таким образом, когда один и тот же запрос отправляется несколько раз (повторяется клиентом), он игнорируется. Однако иногда у нас может быть законный вариант использования для отправки одного и того же запроса дважды (например, покупка двух мечей в течение короткого периода времени).
Временная связь — недоступность клиента
Вы думаете, что вы умны, но PUT с повторными попытками недостаточно. Прежде всего, клиент может умереть при повторной попытке неудачных запросов. Если сервер серьезно поврежден или не работает, повторная попытка может занять минуты или даже часы. Вы не можете просто заблокировать ваш входящий HTTP-запрос только потому, что одна из ваших нижестоящих зависимостей не работает — вы должны обрабатывать такие запросы асинхронно в фоновом режиме — если это возможно. Но увеличение времени повторения увеличивает вероятность смерти или перезапуска клиента, что приведет к потере нашего запроса. Представьте, что мы получили премиальные SMS, но InventoryService в данный момент не работает. Мы можем повторить попытку через секунду, два, четыре и т. Д., Но что, если InventoryService работал в течение нескольких часов и случилось так, что наш сервис также был перезапущен? Мы только что потеряли это SMS и меч никогда не был передан геймеру.
Временная связь — клиент и сервер не могут встретиться
Наша борьба за правильное выполнение повторных попыток является признаком неясной временной связи между клиентом и сервером — они должны жить вместе в одно и то же время. Технически это не обязательно. Представьте, что геймер отправляет электронное письмо с заказом в службу поддержки клиентов, которое они обрабатывают в течение 48 часов, изменяя свой инвентарь вручную. То же самое можно применить к нашему случаю, но заменив почтовый сервер каким-либо брокером сообщений, например JMS:
Установив соединение ActiveMQ, мы можем просто отправить запрос на покупку брокеру:
Недостатки асинхронного обмена сообщениями
Синхронный обмен данными, используемый в ReST, SOAP или любой форме RPC, прост для понимания и реализации. Кому небезразлична эта абстракция, просачивается с точки зрения латентности (локальный вызов метода обычно на несколько порядков быстрее по сравнению с удаленным, не говоря уже о том, что он может потерпеть неудачу по многим причинам, неизвестным локально), он быстро разрабатывается. Одним из настоящих предостережений при обмене сообщениями является канал обратной связи. Вы можете больше просто « отправить » (« вернуть ») сообщение обратно, так как нет ответного канала. Вам либо нужна очередь ответов с некоторым идентификатором корреляции, либо временные одноразовые очереди ответов на запрос. Также мы немного соврали, утверждая, что размещение посредника сообщений между двумя системами устраняет временную связь. Да, но теперь мы подключены к шине обмена сообщениями, которая также может отключиться, тем более что она часто находится под высокой нагрузкой, а иногда и не реплицируется должным образом.
В этой статье показаны некоторые проблемы и частичные решения для обеспечения гарантий в распределенных системах. Но, в конце концов, помните, что семантику « ровно один раз » практически невозможно реализовать, поэтому дважды проверьте, что они вам действительно нужны.
Почему важна идемпотентность и как писать идемпотентные 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.