Как стать автором
Обновить

Проектирование REST API: спорные вопросы с проектов и собеседований на системного аналитика (и не только)

Уровень сложностиСредний
Время на прочтение13 мин
Количество просмотров41K
Всего голосов 25: ↑21 и ↓4+20
Комментарии302

Комментарии 302

Например, что вернете, если при создании нового города в справочнике обнаружили, что город с таким названием уже ранее был создан в БД (найден дубликат)?

А вы справочники вручную заполняете?
Мне когда надо были страны, я импортнул CSV
Из данной репы https://github.com/stefangabos/world_countries
Страны и города у нас не создаются каждый день

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

Здравствуйте!

Можно заменить пример на "Создать задачу, при условии, что запрещено создавать задачи с одинаковыми названиями".

Интересно, как обрабатываются ошибки логики сервера. У кого 400-я, а у кого 500-я на такой тип ошибок идёт. Не обязательно 1 в 1, а похожие.

По идее должен быть 409 Conflict

Согласно описанию 409:

409 Conflict — запрос не может быть выполнен из-за конфликтного обращения к ресурсу. Такое возможно, например, когда два клиента пытаются изменить ресурс с помощью метода PUT. Появился в HTTP/1.1.

Если мы делаем через PUT, то допустимо. Если делаем через POST - в целом тоже. Ошибка про одно и то же :)

В подтверждение Вашего ответа:
https://stripe.com/docs/api/errors - международная платежная система.

409 The request conflicts with another request (perhaps due to using the same idempotent key) = Запрос конфликтует с другим запросом (возможно, из-за использования того же идемпотентного ключа).

Всегда стараюсь искать подтверждение. Почему задала вопрос и не стала оставлять свое мнение: я делала и делаю HTTP 400/500 со специальными кодами ответов, чтобы не разводить зоопарк разных HTTP. Так научили разработчики и архитекторы, с которыми был опыт работы. Хотя правильно делать скорее HTTP -409 или -422.

422 Unprocessable Entity — сервер успешно принял запрос, может работать с
указанным видом данных (например, в теле запроса находится XML-документ, имеющий верный синтаксис), однако имеется какая-то логическая ошибка, из-за которой невозможно произвести операцию над ресурсом. Введено в WebDAV.

А чем 303 See Other плох?

То что имя создваемого клиентом ресурса оказалось не допустимо по любой причине (в том числе и запрет на дубли) - это ошибка клиента, т.к. это он передал это имя в теле запроса. Значит статус ответа должен быть из группы 4хх.
Из имеющихся вариантов больше всего подходит 422 - ошибка валидации входных данных, т.к. проверка на дубликат - это точно такая же валидация, как и проверка на длину строки, вхождение числа в разрешённый интервал или проверка значения на вхождение в список допустимых. При необходимости в теле с ошибкой можно указать ссылку на существущий ресурс, который "мешает" создать дубликат.

Есть конечно доп. возможность - вернуть код 200, вместо 201, и в теле ответа передать уже существующий в системе ресурс. А через заголовок Location - ссылку на этот ресурс. Можно даже применить к существующему ресурсу значения остальных полей, переданных в запросе. Это всё зависит от бизнес логики. Если клиенту абсолютно плевать на то, что на сервере есть дубликат, и он любыми средствами будет добиваться изменения состояния на сервере, то можно оптимизировать этот процесс. Не заставлять клиент получать ошибку от POST метода и потом вызывать PATCH метод, а сразу в POST выполнить нужные изменения и сообщить об этом кодом 200, намекая, что этот запрос не создал новый ресурс.

что вернете, если при создании нового города в справочнике обнаружили, что город с таким названием уже ранее был создан в БД

Я полагаю что ошибка должна быть 422. Ошибка валидации входных данных.

422 Unprocessable Content
The HyperText Transfer Protocol (HTTP) 422 Unprocessable Content response status code indicates that the server understands the content type of the request entity, and the syntax of the request entity is correct, but it was unable to process the contained instructions.

Посмотрел 409.

Код ошибки 409 относится к HTTP-кодам состояния и обычно указывает на "конфликт".

...

Примеры ситуаций, когда может возникнуть такой код ошибки:

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

  • Два пользователя пытаются одновременно создать ресурс с одинаковым именем или идентификатором.

Т.е. тут должно быть какое-то "состояние" и должен юыть какой то "конфликт" и какая-то "одновременность" чтобы конфликт возник. Если просто пытаешься вставить дубликат, то тут мне кажется ещё нет конфликта...

Зависит от API.

Если название — это часть имени ресурса (хотя именно для городов не очень подходит, так как есть много городов с одинаковым названием), то это скорее всего PUT и HTTP 409 Conflict подходит идеально.

Если название — часть состояния ресурса, а в имени только идентификатор, то тут возможны варианты в зависимости от приложения. Логичнее всего вернуть HTTP 303 See Other, который для такой ситуации и предназначен. Если нужно добавить строгости и предполагать, что попытка добавления уже существующего ресурса — это явная ошибка, то тут подходят как HTTP 422 Unprocessable Content (так, например, делает GitHub), так и HTTP 409 Conflict, но оба с небольшой натяжкой.

Меня, конечно, за такое заплюют, но иногда, для своих внутренних нужд я использую что-нибудь типа 302 found с перенаправлением на url найденного объекта

Ну... Если это помогает клиентам разобраться в проблеме, то почему бы и да. Но видимо Ваш API не используется для массовых внешних интеграций К ВАШЕЙ системе.

Нужно пояснение в API-документации и обоснование, почему так делаете. Без обоснования решения заплюют :)

Не затронут момент, который часто встречаю у новичков, избыточный идентификатор родителя в url, пример /item/{item_id}/subitem/{subitem_id}, в большинстве случаев достаточно (а часто и необходимо) только /subitem/{subitem_id}. Конечно же бывают редкие исключения, когда этот идентификатор уникален только в подмножестве своего родителя.

А про 500, только тогда, когда сервер не может сообщить клиенту почему произошла ошибка, точнее даже когда он сам не может это понять.

что вернете, если при создании нового города в справочнике обнаружили, что город с таким названием уже ранее был создан в БД (найден дубликат)?

400 или можно вообще 409
500 тут возможна только когда бд выдала ошибку уровня "IntegrityError duplicate key value violates unique constraint", а разработчики не предусмотрели ее обработки, т.е. это их явная ошибка.

Здравствуйте!

Можете привести чуть более конкретный пример, пожалуйста?

Идею поняла, но не сталкивалась много с подобными ошибками у новичков.

Пример: есть страны, есть города из этих стран, в подавляющем количестве решений, городу сделают уникальный id на всем множестве стран. Новички, разрабатывая api, сначала сделают для стран /country/1/, потом сделают получить все города страны /country/1/city/ и в итоге приходят к /country/1/city/1/

А какой эндпоинт должен быть если вы хотите получить список всех городов страны 1?

/cities?countryId=1

ага, есть два варианта, и обычно эти оба два можно без проблем реализовать в одном проекте
/country/1/city
/city/?country=1

Поддерживаю:

/country/1/city
/city/?country=1

Оба варианта хорошие.

Я бы назвала это проблемой новичков, когда мы с вами точно знаем, что надо три эндпоинта:

  • города

  • регионы (пропустили)

  • страны

И в этом случае, на примере получения данных по списку городов в регионе:

  • GET /country/{countryId}/region/{regionId}/city - перебор

  • GET /region/{regionId}/city - странно, куда потеряли страны?

  • GET /city/?countryId={countryId} и GET /city/?countryId={countryId} - работают одновременно и хорошо для городов и регионов, гибкость, независимость.

Тут новички реально могут что-то терять и упускать, сложная иерархия, а 6-уровневый АПИ не лучшее решение.

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

Круто, когда в команде идеи могут незавимимо накидать и разработчики, и системные аналитики, а затем выбрать лучшее, с обоснованиями.

так вроде сразу написал про /country/1/city/ или это не то?

Руководство по API от Microsoft рекомендует /countries/1/cities

При этом города могут быть самостоятельным ресурсом: /cities/1234

да, вот. не знал как аргументировать))

/cities?countryId=1 - выглядит как то по вордпрессовски...

Конечно же бывают редкие исключения

Кстати, не очень-то и редкие. Например, всё случаи, когда идентификатор задаётся клиентом. Например, GitHub API.

"Избыточная" вложеность может быть оправдана по разным причинам.
Например при использовании шардированной базы данных надо знать item_id, даже если subitem_id глобально уникален. Иначе запрос к базе, без указания фильтра по ключу шардирования, полетит на все шарды и будет создавать не нужную нагрузку.

НЛО прилетело и опубликовало эту надпись здесь

Благодарю за рекомендацию!

Использовал POST вместо GET в трех случаях:
а) Относительно большое количество мелких параметров (а=1, b=2, c=3... z=N). Гарантированно попадаешь в длину URL, но удобнее передать объект в теле запроса.
б) Заказчик получал странные рекомендации от своих "безопасников" касательно того что POST "безопаснее" чем GET по HTTPS. В таких случаях делал две копии endpoint для одного и того-же запроса. И POST и GET юзали одинаковые параметры в заголовках (bearer и так далее)...
в) API для сторонних пользователей, которые "не асиливают" POST в коде на своей стороне и им проще писать "https://server.com?" + "a=1" + "&" + "b=2" + "&" + "c=3", чем морочится с параметрами.

Вообще это наверное больше холивар. Наверное таки да. POST - логично когда запрос меняет данные в базе на сервере. Типа как DELETE. Конечно, запрос на удаление можно и GET'ом сделать. Но логика более прозрачна и понятна.

Конечно, запрос на удаление можно и GET'ом сделать

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

Так про то и разговор. Можно вот так:
GET "https:\\server.com?action=delete&id=1"
но конечно намного лучше
DELETE "https:\\server.com" с параметром "id=1" в теле. Намного понятнее и легче в поддержке.
Я встречал много "я-ж программистов" которые никак не могли осилить POST в javascript например. Для них приходилось дублировать запрос c GET и параметрами в query...

DELETE https://server.com/id=1

А вот не всегда. Если "id" это не "id", а "id's"

DELETE https://server.com/ids=1,2,3,4,5

или

DELETE https://server.com
и в BODY
ids = {1, 2, 3, 4, 5}

в BODY таки более гибко получается.

По гибкости одинаково, зато в девтулзах сразу видно что именно удалялось.

Совершенно с вами согласен. Тут уж от задачи плясать надо. В мелких базах, где не подразумевается "bulk delete" больших объемов - конечно проще "id" [FromRoute] брать. Намного нагляднее получается. Там, где удаляется что-то "большими пачками", все таки из BODY проще.

Когда удаляется несколько записей, это обращение к другому ресурсу.
DELETE /entities/1 это обращение к отдельной сущности, а DELETE /entities обращение к коллекции сущностей.

DELETE относится исключительно к тому ресусрсу, на который указывает url. Параметры запроса, где бы они не были переданы, не имеют отношения к тому что именно надо удалить. Они могут как-то, менять процесс удаления или вообще его отменить, но не выбрать что именно надо удалить.
Если надо удалить пачку разных ресурсов, то такое корректее реализовать методом POST к специальному ресурсу, который можно назвать, например, "items_deleter".
Такой подход будет особенно полезен в тех случаях, когда удаление 100500 ресурсов выполняется долгое время. В этом случае лучше работать по схеме "быстро создать задачу и пускай сервер её выполняет где-то в фоне".

есть вечный вопрос на эту тему) что делать если надо считать количество обращений к какому-либо ресурсу и сохранять это в бд для дальнейшей аналитики? Выходит что гет запрос что-то меняет в бд (хоть и неявно и несинхронно)

Кэшированием можно отдельно управлять через заголовок Cache-Control. Самое "лобовое" решение, чтобы запретить пользоваться любыми кэшами - установить Cache-Control: no-store в ответе на такой запрос.

Тело в GET есть в ElasticSearch API, например.

Ещё помогает сделать дырку в политике CSRF для эндпойнта, если нет возможности политику переписать.

Тело в GET может очень легко в форвардинге потеряться. Аж со свистом. Очень плохая практика кстати.

Про ошибки 400 vs 500 - принцип простой:
ошибки 4xx означают, что если клиент изменит что-то в своем запросе, то он может быть получит результат. https://www.rfc-editor.org/rfc/rfc9110.html#name-client-error-4xx
Ошибки 5xx означают, что от действий клиента ничего не зависит - где-то внутри сервиса при обработке запроса всё идет плохо. https://www.rfc-editor.org/rfc/rfc9110.html#name-server-error-5xx
Однако на практике это означает необходимость ручного анализа возникающих на сервере ошибок и принятия решения по каждой, что выдавать: 400 или 500. Сами понимаете, на это подпишется не всякая команда разработки.

Есть и приколы. Некоторые системы допускают более широкое толкование понятия "действий клиента". Например SaaS позволяющий изменять конфигурацию купленного сервиса. Бывает, что клиент так наизменяет, что сервис выпадает с внутренней ошибкой 500. И для того, чтобы донести до клиента информацию, что он сам может исправить ситуацию, приходится совершенно очевидную ошибку 500 (сервис подняться не может) преобразовывать в 400.

Хороший сервис, но почему-то все запросы реализованы через POST. Верю, что этому есть какое-то объяснение

Банально проще. Метод везде один и тот же, диспатч происходит по аргументу из строки или по ключу JSON-а.

Теперь уже и API аналитики проектируют? Программисты не могут?

Когда начнут классы создавать и за ООП холиварить?

Начиная читать тоже подумал что сейчас начнётся какая то дичь. Но в итоге вполне адекватная статья про действительно неоднозначные моменты при проектировании rest api.

Всё зависит от правил в конкретной компании. API могут проектировать разработчики, системные аналитики или архитекторы. У кого как заведено. Явных противопоказаний я не вижу.

Post /users для создания сущности плохая рекомендация. Выше пример с заменой гета на пост из за большого объёма фильтра, как думаете два одинаковых поинта сочетать?

Post /user - никаких вопросов не возникнет

Вполне укладывается в семантику ООП и коллекций: users.add(user)

Если POST /users вдруг занято получением списка пользователей, то для создания всегда есть POST/users/new

Или в крайнем случае получение списка пользователей можно "повесить" на POST /users/search.

Но POST /user хуже обоих вариантов выше.

Но POST /user хуже обоих вариантов выше.

почему?

@Free_ze я о том, что в статье допускается, что post users может быть занят получением списка

Пришлось делать получение объектов через post /objects/get - сложные фильтры с массивами через тело передаю

В итоге если остальные методы делать как rest - будет некрасиво и не единообразно

Решил тогда всю api-шку сделать как rpc:

Все методы post, глагол в конце url, все идентификаторы - как query параметры

Ну и в этой парадигме в тему пришлось создание сразу нескольких объектов в одном запросе

Ни один администратор спасибо не скажет за RPC. Сопровождение становится сложнее. Когда разработчик использует стандарты HTTP, обслуживать, решать проблемы и наблюдать за работой системы можно и без глубоких знаний деталей реализации. Нам досталась поделка от вендора аля RPC, это худшая система, которую нам доводилось сопровождать. Всё POST, все ответы, любые даже с ошибками, 200 OK, с какой-то трудноразбираемой фигнёй внутри. Поэтому абсолютно невозможно дать какие-то ответы на вопросы, типа масштаб бедствия, процент удачи/неудачи на тот или иной запрос, вообще ничего, а лезть и разбираться в кишки ответов на каждый случай (сотни сервисов) никто не желает, и нафиг это никому не упало.

Не делайте RPC. А если уж делаете, придётся поработать руками и сделать всё то, чего нехватает для мониторинга, да это будет доп. работа, при чём много, но что поделаешь. Такова цена.

Тут я не согласен. Возвращать 200 на любой чих можно и в REST. Это не косяк RPC, а кривые руки разработчика.

ЗЫ А так вообще приятно видеть знакомые ники с sql ru

Согласен, привет-привет! :)

Ну тут да, речь про кривые руки разработчика, просто REST это про передачу состояния, а в условиях HTTP, передача 200, когда у нас очевидная ошибка, это обман, а не передача состояния.

почему так модно винить кривые руки разработчиков а не например головы?

и вообще откуда в ИТ так много криворуких?) куда не глянешь все ругаются на криворуких) кого не спросишь все мидлы/сеньоры и знают свое дело)

Делитесь, когда у вас в компании принято использовать 400-е ошибки (ошибки клиента), а когда 500-е (ошибки сервера)?

Если делается публичное АПИ, то при выборе статуса ошибки стоит учитывать такую семантику: запрос после получения 500-ой ошибки может ретраится пока не получит, что то более осмысленное, а на 400-х прекращать попытки.

Ну и идемпотентность (ключи идемпотентности, как её проявление) стоило бы подробно рассмотреть.

Когда выбирал как делать API тоже изначально думал сделать REST, но в процессе быстро понял что GET метод очень неудобен, у меня много таблиц, пихать фильтры и сортировки в url вообще не вариант, а тела по сути у него нет, + приходится на сервере GET параметры разбирать одним способом, а в других запросах, в которых все в json в теле передается другим, это тоже неудобно, в итоге я вообще не понял почему REST такой популярный, мне он показался очень неудобным.

есть отдельный вид "изысканных" решений)

сначала через пост создается фильтр а потом по этому фильтру (его айди) через гет запрос достаются данные

По идее, get запрос предназначен что бы получить состояние ресурса на который указывает url, например /users. Получение в этом же ответе списка состояний дочерних ресурос (/users/<user_id>) - это уже доп.функционал, который не обязательно реализовывать в рамках get запроса к ресурсу-контейнеру.
Если ваши запросы простые, быстро выполняются, тогда можно использовать get к контейнеру. Если же у вас это что-то сложное, с достаточной вероятностью непредсказуемо долгого времени выполнения, то лучше под это предусмотреть отдельное api. Такое как выше описали - отдельный ресрус "поисковые_запросы", который будет через post создавать ресурсы "запрос", в котором будут возвращаться результаты поиска. При этом не обязательно этот "запрос" сохранять в базу данных, если это не требуется на первых порах. Главное что, api уже поддерживает такую возможность, и в будущем, при усложнении поиска, можно будет реализовать полностью "асинхронную" схему работы поиска не меняя api.

Endpoint /user или /users, Что правильно?

Ответ очевиден. Если хотим получить одного пользователя, используем /user, если получаем список пользователей /users. GET /users/:ID это по юниорски, так как не читаемо.

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

Можно ли использовать всегда POST ?

Например так удобно применять паттерн CQRS. GET запрос, POST команда. Например так более секьюрно: DELETE user/:ID может попробовать отправить любой член клуба юных хакеров потому что это стандартно.

И напоследок: система может быть распределенной из множества микросервисов с шиной данных из например кафка и тогда POST запросом удобно и безопасно создавать обработчик запросов.

Программирование тесно связана с математикой, а в математике все логично и доказательно. Точно также как и при написании RESTFUL.

Как там единственное число от слова goods?

Product

GET /users/:ID это по юниорски, так как не читаемо

Что нечитаемого-то? Директория /users, в ней файл :ID.

Вот если пользователи лежат в директории /user, а их список почему-то в файле /users - тогда-то и получается нечитаемо.

 Например так более секьюрно: DELETE user/:ID может попробовать отправить любой член клуба юных хакеров потому что это стандартно.

Ну да, ну да, старое доброе security through obscurity… Главное - не забудьте убрать спеку OpenAPI из общего доступа :-)

И напоследок: система может быть распределенной из множества микросервисов с шиной данных из например кафка и тогда POST запросом удобно и безопасно создавать обработчик запросов.

Вот вообще связи не вижу.

Endpoint /user или /users, Что правильно?

Ответ очевиден. Если хотим получить одного пользователя, используем /user, если получаем список пользователей /users. GET /users/:ID это по юниорски, так как не читаемо.

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

В примере с единственным числом у Яндекса nearest-settlement – это ведь, наверное, не элемент коллекции nearest-settlements, а результат вычисления. А есть ли какие-нибудь публичные примеры, когда единственное число используется именно для получения элемента коллекции?

А если смотреть на REST API, как на спеку, а HTTP как на траспортный протокол. И не иметь интимностей с особенностями кэширования и проксирования разных HTTP методов.

Когда лучше не использовать GET - если ответ меняется чаще чем раз в день.

PUT и POST для создания/обновления записи в базе, нужно использовать если пользователи вам платят за вызовы API.

В одном сборнике вопросов к собеседованиям прочитал такой вопрос:

Зачем придумали все эти GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD,... (и ещё куча)

и ответ был очень простой:

чтобы тупо не создавать кучу эндпоинтов с вариацией на тему /users и оперецией которую нужно сдедать.

А почему не надо создавать кучу эндпойнтов для операций, которые нужно сделать? Это же разные операции, значит должны быть и разные эндпойнты.

Это типо как иероглифы? Вместо 33 букв из которых мы составляем слова, мы каждое слово делаем уникальным. Это же отдельное слово. В принципе это тоже работает, но говорят что это сложно...

Так вы не составляете из методов GET, POST, PUT, PATCH слова, вы используете только один метод за один запрос. Где же тут сходство?

GET, POST, PUT, PATCH это точно такие же уникальные слова. Вы предлагаете использовать язык из 10 слов для описания всех действий. Это не то что сложно, а в некоторых случаях невозможно, и придется переносить часть информации в тело запроса.

И да, вы не ответили на вопрос.

да, вы не ответили на вопрос

Это прямо зависит от того находите ли вы сходство.

Где же тут сходство?

Ну тут метафора.

Get, Post - предлоги,

/users - корень

Сочетая корень с различными предлогами вы получаете разные слова. Т.е. Вы имеете схему. Просто увидя корень и даже не понимая что он значит вы можете набросать слова которые вы уже будете понимать.

Увидя иероглиф, вы просто видите закорючку.

Это прямо зависит от того находите ли вы сходство.

Нет, это зависит от того, дали вы прямой ответ или нет. Метафора это не прямой ответ.

Сочетая корень с различными предлогами вы получаете разные слова

Ну вот я и предлагаю сочетать корень с большим количеством предлогов, чтобы получить больше слов.

вы можете набросать слова которые вы уже будете понимать.

У меня есть API, в нем есть операция "PATCH /article/1". Вы можете сказать, что она делает - это публикация статьи или редактирование текста? Если нет, значит вы это не понимаете. Значит ваш подход не работает.

А что делает "POST /article/publish/1" - это публикация статьи или редактирование текста? Если вы понимаете, какой из этих двух вариантов правильный, значит мой подход работает.

Увидя иероглиф, вы просто видите закорючку.

Почему набор букв "POST" это предлог, а набор букв "PUBLISH" это иероглиф? Слово publish точно так же является глаголом, как и post.

Вы можете сказать, что она делает

Тут как с языком, зависит от того говорим ли мы с вами на одном.

Patch частичное обновление ресурса,

/articles - ресурс

/1 - идентификатор

Т.е. частичное обновление ресурса с идентификатором 1. Если обновляется только свойство is_published. То я могу понять, что это публикация

"PUBLISH" это иероглиф

Вам /article/publish/1 кажется понятным лишь потому что вы его придумали. Вы удивитесь сколькими способами можно не понять друг друга. Например, поскольку английский возможно не ваш родной, то вы можете необоснованно полагать что англоговорящий подразумевает под publish публикацию. Но это может быть совсем не так. И англоговорящий может мыслить по другому. Я уж не говорю как может мыслить совсем не говорящий на инглише.

Вообще, эти слова get, post идут из веба. Страницу /users получают get /users, форму на этой странице отправляют post /users. По вашей логике, практически для каждого поля формы можно отдельный метод делать. В т.ч. для чекбокса is_publish тоже отдельный экшен. Иероглифы...

Вам /article/publish/1 кажется понятным лишь потому что вы его придумали.

Нет, я как раз объясняю, что "PATCH /article/1" нисколько не понятнее "POST /article/publish/1", как вы пытаетесь доказать. Это точно такой же придуманный язык.

По вашей логике, практически для каждого поля формы можно отдельный метод делать.

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

Мне кажется, что при описании GET vs POST не учтены некоторые нюансы кеширования. Как правило считается, что GET запрос с одним набором параметров возвращает одно и тоже - и его результат можно кешировать. А вот POST по умолчанию нельзя. Так что можно конечно применять POST вместо GET - пока не столкнетесь с тем, что где-то посредине у вас кеширующий прокси.

Мне кажется, любой, кто делал бэкенд с достаточно сложной бизнес-логикой, быстро приходит к выводу, что REST для бизнес-логики не подходит. Какой HTTP-метод надо использовать для действия "Опубликовать статью" - PATCH, POST, PUT? А на бэкенде сравнивать статусы, какой был и какой стал? Зачем делать какие-то механизмы для определения нужного действия, если можно его сразу указать?

Иногда еще советуют добавить фиктивную сущность "Запрос на публикацию статьи" и создавать ее с POST. Зачем она мне на бэкенде, если в бизнес-требованиях ее нет?

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

"что видишь, то и меняешь"

Я вижу статью, у меня нет никакого "запроса на публикацию". Зачем мне менять то, чего нет?

а не 100500 методов типа

Учитывая, что они есть в бизнес-требованиях, и поэтому всё равно должны быть реализованы, я не вижу ни одной причины, почему в API их надо прятать. Мне в любом случае надо вызвать один из них. Как я должен определять, какой именно? Проверять все возможные комбинации всех полей "было/стало"?

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

Вы предлагаете использовать более сложный для меня способ, на вопрос "зачем" говорите "чтобы не делать более простой для меня". Я не вижу тут никакой логики. Вам на фронтенде может и проще просто отправить сущность, а мне на бэкенде это обрабатывать сложнее. Вы предлагаете не убрать сложность, а перенести ее с фронтенда на бэкенд, причем увеличив в несколько раз.

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

Вы мыслите "бизнес-действиями"

Это не я мыслю, это требования бизнеса. Он хочет, чтобы система отправляла письмо пользователю при отклонении статьи модератором, и не отправляла при других действиях.

Вот если будете мыслить "бизнес-состояниями", то заметите, как всё сильно упрощается

Я на это ответил в предыдущем комментарии. Если мыслить бизнес-состояниями, для меня все усложняется. Да, я проверял. Непонятно, почему вы это игнорируете.

один раз прописали гуарды и эффекты на целевые состояния, и уверены, что какими бы путями вы к ним ни пришли, инварианты будут выполнены

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

"Один раз прописали гуарды" означает прописывать их в одном месте для всех возможных сочетаний входных данных. Мне это неудобно. Про это я тоже написал в предыдущем комментарии. Вот если бы вы отвечали на вопросы, то сами бы поняли почему неудобно.

и "а что если при скрытии модератором сервер будет прибит и письмо отправлено не будет?"

Да-да, это как раз хороший вопрос. Модератор скрыл статью, она сохранилась в базу, и теперь у нее статус "Отклонено модератором", а потом на сервере произошел сбой, и письмо не отправилось. С RPC модератор еще раз нажимает кнопку "Скрыть статью", вызывается обработчик этого действия, который идемпотентно устанавливает статус "Отклонено модератором" еще раз и отправляет письмо.

Как нам его переотправить в вашем варианте? А никак, у сущности уже новый статус. Публиковать статью, которую публиковать нельзя, чтобы изменился статус, а потом скрывать? Делать еще одну фиктивную сущность "Запрос на скрытие модератором"? Вместо одной сущности с 10 действиями у нас стало 10 сущностей с одним действием. Не вижу тут никакого упрощения API или кода на бэкенде.

И это мы еще не начинали про загрузку изображений к товару, где валидировать надо данные файла, а в сущности ProductImage есть только свойство url. С RPC это делается тривиально, а с REST начинаются танцы с бубнами.

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

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

C RPC модератор либо видит, что статься скрылась и больше не нажимает кнопку

Он видит сообщение "Что-то пошло не так, попробуйте еще раз", и пробует еще раз. Также в зависимости от реализации он может видеть, что письмо не отправилось.

либо видит ошибку скрытия, сколько бы раз он на кнопку ни нажал

Я не понимаю, откуда вы это взяли, по условиям примера ошибка происходит один раз.

и даже не слать никаких уведомлений, если модератор скрыл статью по ошибке и тут же отменил скрытие

Этих бизнес-требований в моем примере нет. Мне интересно как в этом подходе решать мою задачу, а не какие-то другие.

когда сервис рассылки писем снова начнёт работать

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

Оба варианта плохи с точки зрения бизнеса.

С RPC вы такое сможете реализовать лишь через костыльную материализацию событий в состояние через какую-нибудь Кафку

Что лучше для бизнеса, решать бизнесу, а не вам. У бизнеса нет требования делать переотправку при сбое скрытия статьи автоматически, ни с Кафкой, ни с "Запросом на скрытие". Он сказал, что ему удобнее простая кнопка с сообщением об ошибке. Он не хочет платить за автоматическое повторение действия для каждой кнопки.

Судя по вашим вопросам, с состояниями вы работать не умеете. Так что попробуйте ещё раз. На этот раз более вдумчиво

Если вы хотите объяснить вашу точку зрения, надо отвечать на вопросы в контексте приведенных примеров. Если не хотите, не надо участвовать в дискуссии. Потому что тогда она становится неконструктивной и бессмысленной. Высказывания с умным видом в стиле "Вы нихрена не понимаете, подумайте еще раз" я воспринимаю как то, что ответа у вас нет. И, соответственно, как подтверждение моей точки зрения.

Для этого представьте, что время остановилось, и вам нужно по текущему состоянию мира понять, надо ли отсылать письмо.

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

С RPC ничего понимать не надо. Когда выполняется обработчик "Скрыть статью", мы уже точно знаем, что письмо надо отсылать. Вы утверждаете, что "надо определять" это проще, чем "не надо определять", с моей точки зрения это не выглядит логично.

Например, это может быть флаг "уведомление о скрытии отослано"

Опять какие-то непонятные данные, которых нет в бизнес-требованиях. А как уведомление о скрытии отправить при повторном скрытии? Значит надо где-то сбрасывать флаг, наверно при публикации. Вы же обещали, что это будет проще, а пока получается только сложнее.

В качестве бесплатного бонуса вы сможете легко группировать уведомления

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

В гугловских рекомендациях нестандартные глаголы предлагается добавлять через двоеточие в конце имени ресурса. Это неплохо работает, если нестандартного немного. Но если это действительно проблема, то стоит задуматься, а так ли уж нужен REST. Может, скажем, gRPC (или GraphQL) был бы лучше.

В вашем случае тогда POST /articles. Ведь это новая сущность в БД, не понимаю зачем в данном случае что-то сравнить.

Почему новая? Она уже есть в черновике, у нее есть текст, заголовок и id. Просто она пока не отображается в списке опубликованных. Кнопка "Опубликовать" меняет поле "Статус" с "В черновике" на "Опубликована".

Значит не правильно понял. Тогда такой вариант должен подойти

POST /articles/{id}/states

{"type":"published"}

{"type":"declined", "reason":"text from moderator"}

и сохранять эти мутации стейтов, которые будут менять и стейт поста

У article нет состояния "Declined", это то же самое, что и неопубликованная, то есть в черновике. Модератор скрывает статью из опубликованных и пишет почему скрыл.

Также у article нет свойства reason. То есть это в любом случае не соответствует REST, так как не является representational state сущности article.

Вот если бы при запросе "GET /articles/{id}" среди полей article возвращалось поле states, которое является массивом, то тогда так можно было бы делать. Но тогда непонятно, какой должен быть тип у элемента этого массива. Он будет содержать все возможные параметры всех действий со статьей аналогично reason.

С помощью POST /articles/{id}/states мы создаем условно "action log" сущность, частью которой легко может являться поле reason. Тут полное соответствие REST. Например, когда мы сделаем такой запрос


POST /articles/{id}/states

{"type":"declined", "reason":"text from moderator"}

на бэкенде мы проставим посту статус draft.

И на GET /articles/{id} необязательно возвращать массив states. Все зависит от случая.

мы создаем условно "action log" сущность, частью которой легко может являться поле reason

Я именно про это и написал предыдущем комментарии.
У вас есть одно действие с параметром reason, другое с параметрами count и timeout, третье с параметром email, и еще штук 10 разных действий. Вы все эти параметры будете добавлять в сущность ActionLog? Зачем, если есть способ проще?

И на GET /articles/{id} необязательно возвращать массив states.

Если вы хотите придерживаться Representational State Transfer, то обязательно. Если не хотите, тогда непонятно, почему нельзя просто сделать "POST /articles/{id}/decline".

и еще штук 10 разных действий

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

Если вы хотите придерживаться Representational State Transfer, то обязательно. Если не хотите, тогда непонятно, почему нельзя просто сделать "POST /articles/{id}/decline".

Мы создаем подресурс через POST /articles/{id}/states, соответсвенно и получаем весь список с помощью GET /articles/{id}/states.

Из того же restfulapi.net.

1.2. Collection and Sub-collection Resources

resource may contain sub-collection resources also.

For example, sub-collection resource “accounts” of a particular “customer” can be identified using the URN “/customers/{customerId}/accounts” (in a banking domain).

Similarly, a singleton resource “account” inside the sub-collection resource “accounts” can be identified as follows: “/customers/{customerId}/accounts/{accountId}“.

которое можно решить введением поля metadata

Можно. Но вы не ответили на вопрос "зачем". С RPC ничего решать не надо.

думаю, это искусственное предположение

Это предположение из опыта. Конечно там были не статьи, а другие предметные области.

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

Ну так я ж привел пример, спроектируйте грамотно. Пока что у вас получается запись action_log "Пользователь опубликовал статью" со свойством reason, которое там не нужно.

соответсвенно и получаем весь список с помощью GET /articles/{id}/states

Я про это и написал. Если этой коллекции нет в родительском ресурсе, то это не REST.

A resource may contain sub-collection resources also.

А где там сказано, что в родительском ресурсе customer может не быть свойства accounts?

https://restfulapi.net/rest-api-design-tutorial-with-example/
"Single Device Resource
Opposite to collection URI, a single resource URI includes complete information about a particular device. It also includes a list of links to sub-resources and other supported operations."

Список полей там может быть меньше, чем в ссылке на конкретный под-ресурс, но коллекция должна быть. Ну и вопрос собственно остается в силе - зачем ее туда добавлять, если проще не добавлять и сделать RPC?

Насколько я понимаю, в вашем примере links это немного другое, в тексте говориться о такой штуки как HATEOAS, где в состояние ресурса еще включат секцию links с возможным набором действий с этим ресурсом. В любом случае, наш спор превращается в типичный "этот язык круче, нет этот". На вопрос "зачем" отвечу что вы вольны делать как угодно, удобнее RPC подход, окей, но есть еще и такой подход как REST и мой поинт в том что любое апи можно написать используя данный архитектурный подход. Разные подходы не противоречат друг другу, всегда есть trade-off.

Да нет никакого трейд-оффа) С RPC легко сделать то, что сложно с REST, но нет ничего, что легко сделать с REST, но сложно с RPC. RPC более универсальный подход.

Я не говорил, что API написать нельзя, я говорил, что с REST появляются сложности, которые надо решать, а с RPC их нет. С RPC API получается наиболее близким к предметной области без лишних свойств и сущностей, и бэкенд для него проще.

Мне кажется, любой, кто делал бэкенд с достаточно сложной бизнес-логикой, быстро приходит к выводу, что REST для бизнес-логики не подходит.

Нет, вы утверждали что REST не подходит для сложной бизнес-логики, а я утверждаю обратное :)

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

У меня лично есть опыт переделывания приложения с RPC-style на REST-style, переписывали всё приложение, по нескольким причинам. Переписали и поменяли архитектуру. Стало лучше. Единственный минус, не могу отрицать, некоторым разработчикам пришлось расширить сознание, и научиться мыслить гибче, чем функция(аргументы): результат, это не единственный паттерн, на котором можно строить системы. И чем сложнее системы, тем хуже работает данный паттерн. В играх, например, в принципе на таком паттерне ничего сложнее тетриса сделать не получится.

Написать плохо можно и с RPC, не понимаю, что это должно доказывать. Можно было переписать плохое RPC на хорошее RPC, тоже стало бы лучше. Без примеров такие аргументы не имеют смысла.
Я вообще предлагаю подумать, почему в коде программы мы пишем функции-процедуры с названиями, а не обходимся CRUD-действиями. Наверно это удобнее?

Я бы не стал лукавить и говорить, что стало лучше, потому что переписали. Стало именно лучше, потому что REST. Не хотите верить, я не заставляю и не настаивают. Вы явно не пробовали никогда, и даже не желаете развиваться. Никто вас заставить не может. Да и зачем? Пилите RPC, никто не расстроится. Лучше хоть так, чем вообще никак.

Микроскопом тоже можно гвозди забивать, но все-таки он для этого не подходит.

В данном случае REST совсем не микроскоп, а разновидность молотка.

А мое утверждение со словами "не подходит" о том, что эта разновидность молотка неудобна для забивания гвоздей примерно так же, как микроскоп. Предлагаю перестать играть словами)

Но ведь это вы играете словами, сравнение совсем некорректное, микроскоп не предназначен для забивания гвоздей, а вот REST как раз таки предназначен для проектирования API :)

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

Из-за "легких" аналогий новички делают неверные выводы. Человек который читает наш тред может сделать вывод "писать api на rest это наверное как написать игру на ассемблере,". Но ведь это не так, разница не существенна даже для больших систем.

REST предназначен для проектирования API, но для сложной логики он не подходит

Ну вот опять, что значит не подходит, подходит :)

Что значит "не подходит", я уже объяснил в предыдущих комментариях. Кувалда это тоже разновидность молотка, но для забивания гвоздей она не подходит, хотя ей и можно забить гвоздь.

Но ведь это не так

Именно так, разница существенна. Особенно для больших систем. Для маленьких или учебных конечно и REST подойдет.

Думаю, пора заканчивать этот тред, превращается в "разница существенна", "нет, разница не существенна" :)

Именно поэтому я сделал другой тред, где привел конкретную реализацию на RPC, и те, кому хочется конкретики, могут попробовать сделать такую же логику на REST и сравнить разницу реализаций. Сделайте, сравним) У меня на этот код ушло минут 20.

Зачем фиктивную сущность? Если можно ввести нормальную сущность "Публикация". Которая будет создаваться как и положено через POST и будет содержать указание на статью; пользователя, что публикует; время публикации и т.д. И при этом сама сущность "Статья" не будет в себе содержать избыточных данных. Это также упрощает доработку вашей системы. Если в будущем придется публиковать статью и в других источниках.

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

Если можно ввести нормальную сущность "Публикация".

Можно. Только вы не ответили на вопрос "Зачем?". Зачем мне делать дополнительную работу, если можно ее не делать? Зачем мне намеренно делать код не соответствующим предметной области?

А для модерации создавать нормальную сущность "Модерация"? И так для каждой кнопки в интерфейсе?)

Которая будет создаваться как и положено через POST

При этом также должен меняться статус статьи, которая является другим ресурсом, а это не соответствует принципам REST. То есть у вас в любом случае получается не REST.

И при этом сама сущность "Статья" не будет в себе содержать избыточных данных.

Каких избыточных данных? Хоть с сущностью "Публикация", хоть без нее, сущность "Статья" будет содержать одни и те же данные.

Это также упрощает доработку вашей системы.

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

если бизнес использует не существительное "Публикация", а глагол "Опубликовать"

А вас не соответствие бизнес-глаголов с сущностями смущает только на уровне API? На уровне базы данных вас не коробит, что там по сути только одни сущности, и ни один глагол не совпадает с тем, что написано в бизнес-требованиях?

Не вижу разницы в сложности межу тем, что бы создать 20 уникальных глаголов для двух сущностей, или создать 10 сущностей с 4-мя универсальными глаголами. Уровень сложности на бекенде в общем случае получится тот же самый.
Но второй вариант, за счёт унификации интерфейса и использования функционала хорошо документированного протокола HTTP, позволяет быстрее в нём разобраться тем, кто знает про HTTP, но в первый раз видит ваш API. А это очень важный показатель. Никто ведь не берёт первого попавшегося C++ разработчика, на проект написанный на питоне. И в первую очередь, при прочих равных, предпочтение будут отдавать тем кандидатам, которые знакомы с технологиями и принципами, которые используются в проекте. В этом контексте использование широкоизвестных и документированных технологий и протоколов облегчит поддержку проекта, в отличии от чего-то самописного, которое, как часто бывает, ещё и не описано в документации.

При этом также должен меняться статус статьи, которая является другим ресурсом, а это не соответствует принципам REST.

В REST нет такого, там как раз максимально всё говорит о том, что клиент не должен рассчитывать на какое-то состояние на сервере. Это состояние может измениться там в любое время и по любой причине. Сам сервер может изменить что-то по крон-таске, а может прийти запрос от другого клиента и внести изменения.
Нет никакого запрета на то, что бы приложение на сервере изменило другие сущности в процессе создания/изменения сущности, которую попросил изменить клиент в текущем запросе.

На уровне базы данных вас не коробит, что там по сути только одни сущности?

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

Не вижу разницы в сложности межу тем, что бы создать 20 уникальных глаголов для двух сущностей, или создать 10 сущностей с 4-мя универсальными глаголами.

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

позволяет быстрее в нём разобраться тем, кто знает про HTTP, но в первый раз видит ваш API

А нет такой цели, чтобы разобраться как работает само API, есть цель разобраться как через это API реализуются бизнес-требования.

Бизнес к вам приходит и говорит "Кнопка "Отклонить статью" работает неправильно, разрешает модератору скрыть статью в черновики без указания причины". Вы открываете API, а там только PATCH /article/<id> на любые действия со статьей, и соответствующий метод контроллера в коде. Дальше начинаются выяснения, какой магический JSON надо отправить на этот эндпойнт и как его обработать, чтобы сделать то что нужно бизнесу и не сломать поведение для пользователя.

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

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

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

в отличии от чего-то самописного, которое, как часто бывает, ещё и не описано в документации

Если разработчики не описали в документации к API, что есть RPC-метод "/article/publish", на который можно отправить POST-запрос, что мешает им так же не описать, что есть REST-сущность "PublishArticleRequest", на которую можно отправить POST-запрос? Не вижу принципиальной разницы в том, что документируется, сущности или методы.

Нет никакого запрета на то, что бы приложение на сервере изменило другие сущности

Тут соглашусь, я преувеличил строгость требований REST. Только тогда и никакой упомянутой прозрачности нет, это поведение нужно документировать в виде "Чтобы перевести сущность Article в опубликованные, надо создать сущность PublishArticleRequest, при этом поменяется свойство status у Article".

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

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

  1. Есть мнение, что в случае, когда клиент обеспечивает идемпотентность (сам генерирует ID создаваемого ресурса и его передаёт в сервис через параметр или HTTP-заголовок), то лучше использовать PUT, а не POST.

  2. Код 404 (Not Found) может возвращаться намеренно, когда на стороне сервиса определяется, что клиент не имеет доступа и надо в целях безопасности скрыть сам факт существования сервиса/ресурса.

Ну, за 404 особо не спрячешься. Если вас сканируют и перебирают что-то типа https://server.com/../../../../../ На базовом уровне конечно оно да. Но тут комплексный подход нужен. Я делал так:
https://server.com/api/{magic_number}/v1
Где magic_number это что-то типа одноразового time based токена, который генерируется на клиенте и сервере по одним и тем-же правилам (ну как google auth, например) для каждого запроса. Единственный минус - нужна довольно точная синхронизация времени на клиенте и сервере. Решалось использованием NTP серверов. Конечно учитывается "лаг" по времени и например токен, сгенерированный на клиенте, валиден пять секунд со времени генерации. Если нет - возвращаем 404. Как показала практика - работает. Сканировать перестают.
Конечно, это не для публичного, статического, API. Для "статического" API, разумеется статические API ключи, но ключи "утекают". Делать два запроса для каждого вызова, чтобы получить токен, а потом использовать его в следующем запросе, ну это-ж оверхеад по запросам в два раза...
Было бы интересно почитать о плюсах и минусах такого вот подхода.

По-моему, 404 для сокрытия — это очень специфический случай. Если клиент не аутентифицирован, то нужно вообще на любой запрос возвращать 401. Если аутентифицирован, но мы хотим скрыть остальные ресурсы, то можно возвращать 403 для всего кроме разрешённых ресурсов. А 404 уже использовать, если разрешённый ресурс не найден.

Не такой и специфичный. Он, если не ошибаюсь, даже описан в спеке на HTTP. Там так и сказано про 404, что сервер может возвращать его по любой причине из-за которой нельзя дать понять клиенту, что указанный в URL ресурс существует на сервере. Как пример - отсутствие у клиента прав доступа к ресурсу для которого один факт его наличия на сервере раскрывает приватную информацию. Например если ID ресурса - это любой, публично известный идентификатор человека.

А возвращать 401/403 на любой чих - это, по моему, довольно таки довольно специфичное решение, которое не всегда удобно. Чаще всё таки нет проблем, с тем, что бы клиент без прав доступа узнал, что ресурс на который он стучится - отсутствует.

  1. Не важно на какой стороне генерируется id, для создания ресурсов используется неидемпотентный POST. Идемпотентный PUT используется для изменения состояния ресурса, где нужно засылать на сервер полное состояние, потому что оно должно перезаписаться, в отличие от PATCH.

  2. Если у клиента нет доступа к какому-то ресурсу, то логичнее отдавать 403 Forbidden. 404 Not Found говорит клиент о том что такого ресурса не существует.

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

Как раз-таки важно! Если id генерируется на клиенте, то ничего не мешает использовать идемпотентный PUT и для создания тоже.

А вот для обновления лучше всё-таки использовать PATCH, поскольку у него лучше с обратной совместимостью, а также возникает меньше гонок.

Все так, ничего не мешает использовать PUT вместо POST, но зачем? У данного подхода есть и минусы. Например, придется в коде ветвить логику когда запись новая и когда уже существует. Еще может поменяться логика генерации идентификаторов, в случае с POST легко меняется на бэкенде.

PATCH используется для частичного изменения ресурса, он не обнуляет поля сущности, которые отсутсвуют в запросе в отличие от PUT. Из restfulapi.net:

"The PATCH method is not a replacement for the POST or PUT methods. It applies a delta (diff) rather than replacing the entire resource."

Я в своей практике ни разу не встречал проблем с совместимостью PATCH. Думаю, данная проблема уже давно неактуальна.

Все так, ничего не мешает использовать PUT вместо POST, но зачем?

Как зачем? Ради идемпотентности. А без неё даже и заморачиваться клиентской генерацией идентификаторов нет смысла.

Я в своей практике ни разу не встречал проблем с совместимостью PATCH. Думаю, данная проблема уже давно неактуальна.

А у PATCH никакой проблемы и нет, она есть у PUT.

А у PATCH никакой проблемы и нет, она есть у PUT.

Ни разу не встречал проблем и с PUT.

Попробуйте поредактировать поля через put несколькими пользователями одновременно.

Я уже перечислял эти проблемы, из две.

Во-первых, одновременное редактирование.

Во-вторых, обратная совместимость API. Что делать, если у ресурса появились дополнительные атрибуты, про которые клиент не знает, а потому не может передать? Семантика PUT требует, чтобы их отсутствие воспринималось либо как ошибка валидации, либо как установка в значение по умолчанию.

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

Если вы ни разу не встречали этой проблемы - значит, либо вы никогда не сопровождали готовый продукт, либо у вас не было мобильных клиентов. Особенно "весело" работать с PUT на айфонах, там запрещено блокировать работу устаревших приложений.

Во-первых, одновременное редактирование.

Не понимаю причем здесь одновременное редактирование? Это вообще отдельный вопрос транзакций/уровней изоляции/блокировок и тд.

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

Версионировать API

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

Чего я только не видел за свои 15 лет.

Не понимаю причем здесь одновременное редактирование? Это вообще отдельный вопрос транзакций/уровней изоляции/блокировок и тд.

Чтобы этот вопрос стал вопросом уровня изоляции транзакций - вам надо сначала научиться передавать на бекенд изменения в сущностях, а не всё новое состояние куском. То есть перейти на PATCH.

При использовании PUT у бекенда попросту не будет нужной информации для формирования правильного запроса на обновление, тут никакие транзакции и блокировки не помогут.

Версионировать API

А потом все эти версии поддерживать.

Если у ресурса есть 10 атрибутов, которые добавлялись к нему по одному, то что проще: поддерживать 10 версий api или поддерживать один PATCH?

вам надо сначала научиться передавать на бекенд изменения в сущностях

Я до сих пор не понимаю ваш поинт про PUT и одновременное редактирование, возможно вы имеете ввиду кейс когда есть что-то типа "два юзера редактируют страницу и надо синкать ее состояние", в это случае вам не PUT /doc/{id} надо делать, а POST /doc/{id}/diffs cо своим набором полей.

А потом все эти версии поддерживать.

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

Я хорошо понимаю что всегда есть трейдофф - между "удобно" и "сложно поддерживать", но еще есть мир больших корпоративных систем, где выбор обычно падает на удобочитаемость, понятность, логичность, поддержку стандартов и так далее.

Извините, но чем POST /doc/{id}/diffs принципиально отличается от PATCH /doc/{id}?

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

Так в том-то и дело, что никакой особой магии как правило и не требуется! Патчи-то обратно совместимы.

Извините, но чем POST /doc/{id}/diffs принципиально отличается от PATCH /doc/{id}?

Тем что вы не обновляете ресурс, который нужно синкать, а создаете новый со своим набором полей, а значит у вас нет никаких проблем с как вы сказали "совместным редактированием" и PUT.

Так в том-то и дело, что никакой особой магии как правило и не требуется! Патчи-то обратно совместимы.

Я за свои 15+ лет в разработке, ни разу не встречал такого подхода и проблему с PUT, а вы говорите что никакой магии. А видел и проектировал я ооочень много апишек.

Так ведь и в случае с PATCH /doc/{id} не будет никаких проблем с совместным редактированием и PUT, за неимением/неиспользованием этого самого PUT.

Давайте сначала. Вы утверждаете что в некоторых кейсах PUT неудобно использовать и лучше подходит PATCH. Я утверждаю что проблема не в PUT, а лучше придерживаться "спецификации" REST, и проектировать апи "правильно". Вы вольны использовать любой http метод для вашего удобства, но это уже неявно и вы получите какое-то количество проблем из-за этого в будущем. Да даже простой пример, будут приходить новые люди в команду и вы будете каждый раз рассказывать почему выбрали PATCH и почему отошли от "стандартов".

Ну так чем вам PATCH не "стандарт"-то?

Ну так чем вам PATCH не "стандарт"-то?

Я где-то утверждал обратное?

В описании REST прописано, что для полного обновления ресурса используется PUT, для частичного - PATCH. Вы выбрали у себя частично обновление с помощью PATCH, потому что вам надо частично обновлять, ок. Почему PUT плохой?

Почему PUT плохой?

Потому что нужно частичное обновление.

Вот реализация RPC на примере API для публикации статей. 110 строк вместе с контроллером. Попробуйте реализовать это поведение в виде REST.

PHP
class Article {
  public int $id;
  public string $title;
  public string $text;
  public StatusEnum $status;
}

class ArticleService {
  public function save(SaveInput $saveInput, Article $article): Either {
    $article->title = $saveInput->title;
    $article->text = $saveInput->text;
    
    if ($article->status === StatusEnum::Published) {
      $errors = $this->validateForPublish($article);
      if (!empty($errors)) {
        return new Either(false, $errors);
      }
      // нет записи в лог, статья уже опубликована
    }
    
    $this->entityManager->save($article);
    $this->entityManager->flush();
    
    return new Either(true, []);
  }
  
  public function publish(Article $article, User $user): Either {
    $errors = $this->validateForPublish($article);
    if (!empty($errors)) {
      return new Either(false, $errors);
    }
    
    $article->status = StatusEnum::Published;
    $this->entityManager->save($article);
    
    $message = 'Статья опубликована';
    $this->entityManager->save(new ArticleLog($message, $user));
    
    $this->entityManager->flush();
    
    return new Either(true, []);
  }

  public function decline(DeclineInput $declineInput, Article $article, User $moderator): void {
    $article->status = StatusEnum::Draft;
    $this->entityManager->save($article);
    
    $message = 'Статья отклонена модератором по причине: ' . $declineInput->reason;
    $this->entityManager->save(new ArticleLog($message, $moderator));
    
    $this->entityManager->flush();
    
    $this->mailer->sendDeclineEmailToAuthor($article, $declineInput->reason);
  }

  private function validateForPublish(Article $article): string[] {
    $errors = [];
    if (strlen($article->title) === 0) {
      $errors['title'] = 'Для публикации заголовок должен быть указан';
    }
    if (strlen($article->text) === 0) {
      $errors['text'] = 'Для публикации текст должен быть указан';
    }
    
    return $errors;
  }
}

class SaveInput {
  public string $title;
  public string $text;
}

class DeclineInput {
  #[Assert\Required(message: 'Укажите причину отклонения')]
  public string $reason;
}

class ArticleController {
  #[Route('/article/<id>/save')]
  public function save(Request $request) {
    $article = $this->findEntity($request->query->get('id'));
    $saveInput = new SaveInput($request->body->get('title'), $request->body->get('text'));
    
    $result = $this->articleService->save($saveInput, $article);
    
    return new JsonResponse(empty($result->getErrors()) ? 200 : 400, $result->getErrors());
  }
  
  #[Route('/article/<id>/publish')]
  public function publish(Request $request, #[CurrentUser] User $author) {
    $article = $this->findEntity($request->query->get('id'));
    
    $result = $this->articleService->publish($article, $author);
    
    return new JsonResponse(empty($result->getErrors()) ? 200 : 400, $result->getErrors());
  }
    
  #[Route('/article/<id>/decline')]
  public function decline(Request $request, #[CurrentUser] User $moderator) {
    $article = $this->findEntity($request->query->get('id'));
    $declineInput = new DeclineInput($request->body->get('reason'));
    $errors = $this->validator->validate($declineInput);
    if (!empty($errors)) {
      return new JsonResponse(400, $errors);
    }
    
    $this->articleService->decline($declineInput, $article, $moderator);
    
    return new JsonResponse(200);
  }
}

Вы уверены что это RPC?

Удалённый вызов процедур — класс технологий, позволяющих программам вызывать функции или процедуры в другом адресном пространстве. Обычно реализация RPC-технологии включает два компонента: сетевой протокол для обмена в режиме клиент-сервер и язык сериализации объектов.

Т.е. вы с одного компа передаёте объект класса на другой комп и там вызываете какой-то метод у ещё какого то класса.

А у вас обычный http контроллер.

https://aws.amazon.com/compare/the-difference-between-rpc-and-rest/

RPC
POST /addProduct
POST /getProduct
POST /updateProductPrice

REST
POST /products
GET /products/123
PUT /products/123

При этом подразумевается, что в REST поля входных данных могут быть только полями ресурса-сущности.

Вот только браузер ваши POST кешировать не будет, как и прокся. И сложнее вариант получить данные по ссылке так как нужно еще и тело составить по формату.

Еще вариант вообще 1 эндпоинт и имя функции в теле :)

Но если браузер не подразумевается то gRPC или SOAP норм.

Это цитата со страницы, можно делать и GET если надо. Только такое кеширование часто создает больше проблем чем решает. Мы обновили цену, запрашиваем товар, а там цена не изменилась, потому что данные закешировались. Лучше кешировать кодом на клиенте и вообще не отправлять HTTP-запрос пока данные считаются актуальными.

POST /articles -- создать статью
PUT /articles/{id} -- обновить статью (ваш save)
PUT /articles/{id}/state -- обновить состояние статьи (publish/declain/etc. внутри)

REST это не вызов функции, это запрос-ответ при взаимодействии с ресурсом

путь в REST -- это ресурс, а не имя функции

поэтому, объявляете не действие, а ресурс, глагол при этом является действием над ресурсом

так, API получается валидным и изящным, так как политики применяются над ресурсами, этим легко управлять и парадигма, понятная для клиентов

путь в REST -- это ресурс, а не имя функции

Ага, только кроме пути надо передавать еще и данные, которые в REST должны быть подмножеством полей ресурса.

поэтому, объявляете не действие, а ресурс

Не нужно давать советы, покажите код) Этот пример про бэкенд, а не про то, как выглядит URL.

На пыхе не умею, сорри.

Кроме того, меня смущает #[Route('/article/<id>/save')], я не понимаю какой глагол здесь принимается, или пофиг, хоть DELETE? :)

Напишите на любом. Действия на изменение, значит POST.

Грубо, как-то так. Роут у меня только один, не надо манки-кодить "/api/articles..." для каждого метода. Невозможно сделать PUT-запрос для метода, который принимает POST. Все входящие данные валидируются, возвращаются правильные ответы.

[Route("api/articles")]
public class ArticlesController : ApiController
{
  private IArticlesService _articlesService;

  public ArticlesController(IArticlesService articlesService)
  {
    _articlesService = articlesService;
  }

  [HttpGet("{id}")]
  public async Task<IActionResult> GetDetails(long id, CancellationToken ct)
  {
    var article = await _articlesService.FindByIdAsync(id, ct);
    if(article is null) return NotFound("Article not found");
    var response = article.AdaptTo<ArticleDetailsModel>(article);
    return Ok(response);
  }

  [HttpPost]
  public async Task<IActionResult> Create(ArticleCreateRequest request, CancellationToken ct)
  {
    var result = await _articlesService.CreateAsync(request.AdaptTo<ArticleCreateData>(), ct);
    if(!result) MapError(result); // транслируется в 400,404,422, зависит от типа ошибки
    var response = new ArticeCreateResponse(result.Id);
    return Created(response);
  }

  [HttpPut("{id}")]
  public async Task<IActionResult> Update(long id, ArticleUpdateRequest request, CancellationToken ct)
  {
    var result = await _articlesService.UpdateAsync(id, request.AdaptTo<ArticleUpdateData>(), ct);
    return result ? Ok() : MapError(result);
  }

  [HttpPut("{id}/state")]
  public async Task<IActionResult> ChangeState(long id, ArticleChangeStateRequest request, CancellationToken ct)
  {
    var result = await _articlesService.ChangeStateAsync(id, request.AdaptTo<ArticleChangeStateData>(), ct);
    return result ? Ok() : MapError(result);
  }
}

Так вы код сервиса тоже приведите, непонятно, что обертки должны доказывать. Create и GetDetails не нужны, нужны 3 действия - сохранение текста, публикация, отклонение модератором.

На всякий случай, enum ArticleState содержит 2 значения - "В черновике" и "Опубликована". Значения "Отклонена" там нет, при отклонении статья переводится в состояние "В черновике".

Обёртки показывают принцип. Состояние статьи это ресурс. Никакого смысла показывать код сервиса нет, для демонстрации принципа. Этого достаточно. Присылаете новое состояние, где ArticleState должен иметь соответствующее количество значений, а не как у вас. Т.е. нужен статус Declained, иначе непонятен результат, состояние должно чётко отражать результат действия, поэтому с вашим решением не могу согласиться.

А я говорю про сложность реализации бэкенда при следовании этому принципу. В реализации ChangeStateAsync и будет основная сложность.

Т.е. нужен статус Declained

Нет, бизнес сказал, что статус Declined ему не нужен, он ничем не отличается от статуса "В черновике".
Ну то есть видите, с RPC нет никаких проблем реализовать эти требования, а с REST оказывается, что требования бизнеса какие-то неправильные, и для сущности нужны какие-то дополнительные статусы, которых нет в требованиях.
Собственно, вы предлагаете тот же самый RPC, только название метода кодируется в поле данных, а не в URL.

состояние должно чётко отражать результат действия

Оно у меня и отражает. Есть бизнес требование "При отклонении модератором переводить статью в черновики и писать причину в историю действий". Мой код так и работает.

Видимо кто-то не согласен, объясню подробнее.

Для действия "Отклонение модератором" у нас есть параметр "Причина отклонения", поэтому в ArticleChangeStateData должно быть не только свойство state, а еще и reason. Если у нас 10 действий со статьей, и в каждом по два параметра аналогично reason, то ArticleChangeStateData превратится в God-object на 20 полей. Причем это не будет соответствовать принципам REST, в которых подразумевается, что для PATCH-запроса должны быть указаны поля ресурса, которые мы хотим изменить, а в ресурсе Article этих полей нет. И это сразу становится понятно, если попытаться это реализовать.

Нет никаких проблем. Модератор переводит статус в Чёрновик. Визуально это может быть кнопкой Отклонить. Но на бекенд передаётся установка статуса Черновик. С RPC тоже нет проблем, это другой подход. Этот подход называется "Большой клубок чёрной магии" :) Т.е. вы прям вызываете метод "Отклонить", он что-то делает и совершенно неизвестно к чему приводит с точки зрения клиента. По REST-у, намерения максимально прозрачные, и глядя на запрос-ответ в логах, даже человек, совершенно не разбирающийся в недрах логики, сможет сказать что к чему. Это очень удобно для клиента, для сопровождения, администрирования и девопс. Используя подход RPC, нужно прикладывать тонну документации, или звать разработчика на разбор любых инцидентов.

Мне нравится RPC в подходе backend-backend, например, в gRPC. Но для взаимодействия frontend-backend, практика показывает, что подход REST значительно лучше. Это просто по опыту. Тут материала хватит на несколько статей.

По факту развития и сопровождения множества систем, и весьма крупных, приносящих большие прибыли компании, сопровождать RPC-style проекты сложнее и дороже. Ваш опыт может отличаться, разумеется:)

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

Меня правда беспокоит, что действительно очень многие разработчики совершенно не понимают REST, думая что это просто использование GET/POST/PUT.. и какие-то там соглашения к путям запросов. Огромная печаль просто.

Но на бекенд передаётся установка статуса Черновик

Передавать нужно еще и причину отклонения. Как это сделать и задокументировать, если такого свойства нет в Article?
Как это отличить от скрытия в черновик пользователем? Нам не надо, чтобы пользователь сам мог установить причину отклонения. И не надо отправлять email, если пользователь скрыл в черновик сам.

Т.е. вы прям вызываете метод "Отклонить", он что-то делает и совершенно неизвестно к чему приводит с точки зрения клиента. По REST-у, намерения максимально прозрачные, и глядя на запрос-ответ в логах

POST /article/1/decline
{reason: "Test"}

PATCH /article/1
{status: "Declined", reason: "Test"}

Почему первый запрос более неизвестный чем второй? В них абсолютно одинаковые данные.
Как вы по второму запросу без тонны документации определите, что он отправляет email?

POST /article/1/decline
{reason: "Test"}

В каком состоянии система окажется в итоге? Что будет, если вызвать метод дважды? Что вообще этот метод делает? Вот с точки зрения человека, который занимается эксплуатацией? Может ли вообще в результате этого действия удалиться статья? Да! Или создаться какой-нибудь заказ? Да, почему нет? Никаких предположений сделать нельзя без подробной документации.

PUT /article/1/state
{status: "Declined", reason: "Test"}

Я немного поправил, всё же PATCH здесь совершенно неуместный глагол.
Очевидно, что изменяется статус на указанный, с указанной причиной. И при успехе метода, статья будет иметь именно такое состояние, и никакое другое. Сигнатура совершенно прозрачная и передаёт ожидаемое состояние, к которому должен привести метод. При чём, данная операция по факту идемпотентная, т.е. это действие можно выполнить 2 раза с одним и тем же результатом.

Как вы по второму запросу без тонны документации определите, что он отправляет email?

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

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

А вот как может выглядеть валидный RPC-метод:

POST /articles/decline
или 
POST /decline-article
{ id: 1, reason: "Test", ... }

Никакие данные не должны передаваться в путях. Путь -- это чистое имя метода, сегменты пути могут выражать неймспейсы/модули/методы. Тогда это валидно для RPC. Зачем же смешивать не смешиваемое?

В каком состоянии система окажется в итоге? Что будет, если вызвать метод дважды? Что вообще этот метод делает?

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

Я немного поправил, всё же PATCH здесь совершенно неуместный глагол.

Как раз для одного поля сущности надо использовать PATCH. PUT надо использовать если передается сущность целиком
https://stackoverflow.com/a/34400076
"The entity you are supplying is complete (the entire entity)."

И при успехе метода, статья будет иметь именно такое состояние

Да нет, почему? В ней же нет поля reason. reason пишется только в историю изменений. Так вот бизнес захотел.

т.е. это действие можно выполнить 2 раза с одним и тем же результатом

Да нет, в зависимости от реализации может появиться и 2 записи в истории, и отправиться 2 email. История изменений это тоже ресурс.

То, что вы отправляете, то вы и получите в случае успеха.

Нет, это никак не гарантируется. Вы отправили статус "Declined", а сущность перешла в состояние "Draft", потому что я сделал такую реализацию.

ресурсы определены не верно

Ресурс у нас один - статья. С ней можно сделать 10 действий, которые меняют состояние статьи. Каждое действие имеет параметры, которых нет в ресурсе Article.
Если вы сделаете какой-то другой ресурс, и при запросе на него будете менять состояние Article, то не будет работать правило "То, что вы отправляете, то вы и получите".

Никакие данные не должны передаваться в путях.
Зачем же смешивать не смешиваемое?

Ну как это не должны, самый простой пример - фильтр коллекции в URL.

Это удобно обрабатывать на бэкенде. Когда в URL передается неправильный id статьи, надо возвращать 404, а когда в данных запроса передается неправильный id категории, то надо возвращать 400 с описанием ошибки для поля category_id. RPC не запрещает иметь ресурсы и ссылаться на них в URL.

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

Нет, не справедливы. Если вы делаете именно REST, ты вы не можете удалять статью или делать что-то не очевидное.

И наверное в этом и есть корень вашего непонимания, что сводит дальнейшую дискуссию на нет. Если вам не нравится REST, и нравится RPC, делаете RPC, но не делаете франкештейна.

Как раз для одного поля сущности надо использовать PATCH. PUT надо использовать если передается сущность целиком
https://stackoverflow.com/a/34400076
"The entity you are supplying is complete (the entire entity)."

Вы опять не поняли. Речь не идёт о сущностях, как они у вас определены в базе данных. Вы создаёте ресурсы, в том количестве, сколько вам нужно. В данном случае "Состояние статьи" это цельный ресурс, а не сущность. Даже если у вас в системе нет отдельной таблички или объекта, который это представляет именно так. Но с точки зрения ресурсов в REST, вы делаете мета-ресурсы.

И таким образом, любая задача достаточно легко и прозрачно решается, с понятной логикой и с понятными ожиданиями.

Да нет, почему? В ней же нет поля reason. reason пишется только в историю изменений. Так вот бизнес захотел.

Вы обратили внимание на URL, что ресурсом является не статья, но состояние статьи? А надо было.

Ресурс у нас один - статья. С ней можно сделать 10 действий, которые меняют состояние статьи. Каждое действие имеет параметры, которых нет в ресурсе Article. Если вы сделаете какой-то другой ресурс, и при запросе на него будете менять состояние Article, то не будет работать правило "То, что вы отправляете, то вы и получите".

Я не вижу никаких противоречий. Много лет в разработке систем различной сложности, проблем с тем, чтобы сделать качественный REST у меня не было.

Это удобно обрабатывать на бэкенде. Когда в URL передается неправильный id статьи, надо возвращать 404, а когда в данных запроса передается неправильный id категории, то надо возвращать 400 с описанием ошибки для поля category_id. RPC не запрещает иметь ресурсы и ссылаться на них в URL.

В RPC вы можете делать что угодно и как угодно. Я видал возврат исключений в ответ 200 OK, и прочий трешак. По сути, RPC не имеет никаких соглашений, и когда я вижу различные реализации RPC, у них нет ничего общего, их дизайн отвечает исключительно фантазиям конкретного разработчика.

А когда разработчик увольняется, приходит другой, следующий наворачивает свои фантазии, которые не совместимы с предыдущими (фатальный недостаток). Итого имеем что-то совершенно не сопровождаемое, и работающее по принципу "хрен его знает", даже предполагать что-то невозможно.

Нет, не справедливы. Если вы делаете именно REST, ты вы не можете удалять статью или делать что-то не очевидное.

Есть такое слово - "надо". Вам надо сделать некоторое действие в ответ на определённое действие пользователя.

Надо так надо, не буду с вами спорить. Мы знаем, что когда "надо", можно нарушать любые правила, да и зачем они нужны? Правила какие-то, стандарты, соглашения... только проблемы создают :)))

Не понимаю вашего сарказма. Ну да, бизнес-процессы, сценарии использования, безопасность - кому оно всё нужно? Просто не будем реализовывать ничего что не вмещается в парадигму REST, если, так уж вышло, случайно выбрали её при старте проекта.

Я вас ни в коем случае не убеждаю делать что-то используя ту или иную парадигму. Речь вообще не о том.

Если вы делаете именно REST, ты вы не можете удалять статью или делать что-то не очевидное.

Да не надо мне делать именно REST, это вы говорите, что я почему-то должен делать REST. Мне надо сделать то, что хочет бизнес.

проблем с тем, чтобы сделать качественный REST у меня не было.

Или вы думаете, что делали качественный REST, а на сaмом деле принципам REST оно не соответстовало)

Вы обратили внимание на URL, что ресурсом является не статья, но состояние статьи?

В бизнес-требованиях у состояния статьи нет свойства "reason". Бизнес его выражает строкой "В черновике" или "Опубликована".
Также в этой модели свойство reason есть у всех возможных состояний статьи, что тоже не соответствует требованиям.
Вот как раз таких случаях и требуется куча документации, чтобы разбираться как требования выражены через API, куда там надо отправлять POST, чтобы скрыть статью в черновики, и зачем нужна reason для записи "Пользователь опубликовал статью".

По сути, RPC не имеет никаких соглашений
и работающее по принципу "хрен его знает"

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

RPC не имеет никаких соглашений, их дизайн отвечает исключительно фантазиям конкретного разработчика.

Тип данных ArticleState со свойством reason это тоже фантазии конкретного разработчика. Мне бы в голову не пришло делать модель данных, настолько не соответствующую бизнес-требованиям.

Мне надо сделать то, что хочет бизнес.

Это не оправдание. Если вы зачем-то принимаете для себя решение делать REST, но в итоге делаете что-то другое, при чём тут бизнес? Это как выбрать гвозди для обоев вместо клея, потому что не получилось разобраться как работать с этим клеем, оправдывая "так хочет бизнес". Не могу с этим согласиться.

Если вам не подходит REST, не используйте его.

В бизнес-требованиях у состояния статьи нет свойства "reason". Бизнес его выражает строкой "В черновике" или "Опубликована".
Также в этой модели свойство reason есть у всех возможных состояний статьи, что тоже не соответствует требованиям.
Вот как раз таких случаях и требуется куча документации, чтобы разбираться как требования выражены через API, куда там надо отправлять POST, чтобы скрыть статью в черновики, и зачем нужна reason для записи "Пользователь опубликовал статью".

Ну вы так и не поняли концепцию "ресурс". Да и не желаете понимать. А у меня нет цели что-то вам доказывать.

Но вы можете влепить мне очередной минус :)

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

Я вам показал как это можно сделать, соблюдая REST. И объяснил почему. И это работает, никак не конфликтует с бизнесом, и не мешает решать бизнес-задачи, при этом решая попутно и технические задачи.

Если вы зачем-то принимаете для себя решение делать REST, но в итоге делаете что-то другое, при чём тут бизнес?

Вы говорите, что я должен использовать REST, потому что там все ясно.
А потом оказывается, что это я должен делать дополнительную работу, чтобы было ясно.
Тогда при чем тут REST? Ясно можно сделать и в RPC.

Ну вы так и не поняли концепцию "ресурс".

Я говорю исключительно про ресурсы. Я не знаю, откуда вы взяли, что я говорю про структуру таблиц в БД.

Но вы можете влепить мне очередной минус :)

Я вам не ставил ни одного минуса.

Я вам показал как это можно сделать, соблюдая REST.

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

А цель этой ветки была в том, что бы показать, насколько с REST усложняется реализация. Реализацию вы не показали.

никак не конфликтует с бизнесом

Это ложное утверждение. Я смотрю API, вижу там запись "Пользователь опубликовал статью" со свойством reason. Спрашиваю у бизнеса, зачем нужна причина публикации статьи, он говорит "Я не понимаю, о чем вы говорите, у нас нет требований указывать причину для публикации статьи".

В RPC это явно видно, процедура '/article/1/publish' не принимает никаких данных, процедура '/article/1/decline' принимает структуру с одним свойством reason.

И наоборот, бизнес использует 2 состояния статьи, а у вас их 10, и он приходит и спрашивает "Что это за статус Declined? Мы его не просили, мы просили переводить статью в статус Draft".

А цель этой ветки была в том, что бы показать, насколько с REST усложняется реализация. Реализацию вы не показали.

Ладно, давайте немного притормозим и остановимся на этом. С чего она усложняется? Она просто другая. Да, REST это не просто другой способ представить один и тот же контракт, это и другой способ реализации.

Архитектурно, мы работаем с состояниями и презентациями этих состояний. Любое взаимодействие с системой -- это получить состояние или изменить состояние.

И наоборот, бизнес использует 2 состояния статьи, а у вас их 10, и он приходит и спрашивает "Что это за статус Declined? Мы его не просили, мы просили переводить статью в статус Draft".

Давайте посмотрим:

PUT /article/1/state
{
  status: "Draft",
  reason: "Статья содержит ошибки, надо исправить"
}

Я видимо сам сбился с толку и сбил с толку вас. Нет необходимости вводить новый статус, если его нет у бизнеса.

Вот, собственно, мы меняем ресурс "Состояние статьи", в котором указываем к какому статусу хотим привести. Поля status и reason являются атрибутами ресурса "Состояние статьи". Мы их меняем.

Архитектурно и реализация будет отличаться от вашей. После успешного выполнения запроса PUT /article/1/state, валидно иметь метод GET /article/1/state, который будет возвращать,... да-да, тоже самое:

GET /article/1/state

200 OK
{
  status: "Draft",
  reason: "Статья содержит ошибки, надо исправить"
}

Мы можем сделать copy-paste, изменить и получить новый результат. Это одни и те же модели данных, по сути чистый CRUD :)

Нет, это не сложнее, это другой подход. Есть у этого подхода свои весомые преимущества. Но есть и недостатки. Пилить RPC проще, просто потому что это ближе к бекенд-разработчику, где у него есть функции/процедуры, и всё строится на них. Так разработчику привычнее, с этим нельзя спорить.

С чего она усложняется?

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

status: "Draft", reason: "Статья содержит ошибки, надо исправить"

Тогда и пользователь может поместить свою статью в черновики с какой-то причиной, а это противоречит требованиям. А для модератора причина должна быть заполнена. Да-да, дальше начинается добавление доступа на свойства и прочая муть другие усложнения.

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

Тогда и пользователь может поместить свою статью в черновики с какой-то причиной, а это противоречит требованиям.

Не вижу противоречий. Ваше действие "Отменить" приводит к каким-то изменениям, к определённому результату. Этот результат можно сразу оформить явным изменением. Что здесь вообще сложного?

Какие моменты тут ещё обсуждать? Было бы что! Это не религиозный вопрос, вы либо понимаете простейшую как тапок концепцию, либо нет. А я вижу, что нет. Чем вам код поможет? Пара проверок и UPDATE, серьёзно? Вы вчера программировать начали? Уж извините, накипело, одно и то же, как в стену.

Этот результат можно сразу оформить изменением.

Я не понял, что вы имеете в виду. У пользователя не должно быть возможности указывать причину помещения в черновик. Как у вас это гарантируется?

Какие моменты тут вообще обсуждать?

Те, по которым ваше решение не подходит.

Чем вам код поможет?

Он поможет вам получить ответ на вопрос "С чего она усложняется?". Потому что объяснение словами вы не понимаете. Вы только что предложили решение, которое выполняет одно бизнес-требование и противоречит другому.

Нет, там будет не только "Пара проверок и UPDATE". И судя по тому, что вы предпочитаете писать комменты, когда написать код было бы быстрее, вы это сами понимаете.

Я не понял, что вы имеете в виду. У пользователя не должно быть возможности указывать причину помещения в черновик. Как у вас это гарантируется?

PUT /article/1/state
{
  status: "Draft",
  reason: "Статья содержит ошибки, надо исправить"
}

Помещаем в черновик, указываем причину. Если причину указывать не надо, возвращается ошибка, указывающая какое поле имеет недопустимое значение в формате Problem Details.

Какое требование не выполняется? Я в полном недоумении :)

В каком состоянии система окажется в итоге? Что будет, если вызвать метод дважды? Что вообще этот метод делает? Вот с точки зрения человека, который занимается эксплуатацией? Может ли вообще в результате этого действия удалиться статья? Да! Или создаться какой-нибудь заказ? Да, почему нет? Никаких предположений сделать нельзя без подробной документации.

Документация - да, нужна. API без документации - это хрень, а не API.

А вот остальные вопросы вызывают удивление.

В каком состоянии система окажется в итоге?

В требуемом. Если нужны подробности - документация в помощь. Или исходный код на худой конец.

Что будет, если вызвать метод дважды?

Статья будет отклонена модератором дважды.

Что вообще этот метод делает?

Отклоняет статью, очевидно же. Если нужны подробности - документация в помощь. Или исходный код на худой конец.

Может ли вообще в результате этого действия удалиться статья?

Какая разница? Если решили что отклонённые статьи удаляются - удалится, если решили что не удаляются - не удалится. Что именно решили - см. документацию. Или исходный код на худой конец.

Или создаться какой-нибудь заказ?

Это странно, но тоже возможно. Заказ не заказ, но вот интеграцию с таск-трекером, которая автоматически ставит задачи на исправление отклонённых статей я могу представить.

А вот есть там интеграция или нет - смотри документацию. Или исходный код на худой конец.

Документация - да, нужна. API без документации - это хрень, а не API.

Ну давайте называть методы так FAS312, а свойства так SA2_asddf3211_1. И прикладывать документацию :) Чтоб было понятно, что FAS312 это отклонение статьи, SA2_asddf3211_1 это причина отклонения :)

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

Если я делаю PUT к ресурсу, указывая определённые данные, то в случае успеха, ресурс будет в том состоянии, к которому явным образом к нему приводили.

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

Вы правда не видите разницы между SA2_asddf3211_1 и POST /article/1/decline?

Я правда вижу, что POST /article/1/decline плохое решение, так как выглядит как REST, но REST-ом не является. И это плохо, так как сбивает с толку.

POST /article/decline нормально, если все методы будут такими. Мне не по душе франкенштейны, из области ни туда, ни сюда.

И да, я правда считаю, что PUT /article/1/state является полностью самодокументируемым, очевидным и простым решением. У меня получается делать такие интерфейсы для систем разных сложностей, и выгоду от этого не просто ощущаю, я её вижу.

Конечно, если мы говорим о крупных продуктах, кросс-командной разработке, а не о личном пет-проджекте, или маленьком бизнес-инди проекте, где вообще всё равно, как будет реализован продукт, если цели бизнеса достигнуты.

Также не вижу ничего плохого в RPC, но чётко знаю и понимаю преимущества и недостатки подхода REST. Я не буду их смешивать. Мне это не нужно. А бизнесу и подавно.

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

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

Ну как же не отражает, если напрямую отражает?

PUT /article/1/state -- изменить состояние статьи, в теле указано новое состояние. При чём AS IS. Что делаем = то и получаем. Вот прям в доску простой и прозрачный процесс по контракту.

Да и причин не делать вот так POST /article/decline -- я не вижу. Теперь мы выполняем некое волшебное действие, а не явно меняем состояние. Это другой подход к построению интефрейсов. Но такой подход требуется более плотно документировать. Ведь и правда, что будет в результате данного действия предсказать нельзя по контракту. Во многих аспектах при чём.

Ну как же не отражает, если напрямую отражает?

Я уже 10 раз объяснил - бизнес использует только 2 статуса "Draft" и "Published", а вы предлагаете добавлять "Declined". Бизнес использует статус статьи как строку, а вы предлагаете делать ее структурой со свойством reason, которое нужно лишь для статуса "Declined".
Какое бизнес-требование отражает статус Declined? Никакое. Поэтому не напрямую.

Может хватит уже врать? Я не использую только эти статусы. Я использую и "на проверке", и "на согласовании" со списком ревьюверов, и даже "удалено" с возможностью восстановить. Вы нихрена в бизнесе не разбираетесь, код ни читать, ни писать не умеете, вас только на бессмысленные срачи на хабре хватает.

Я не писал, что вы используете только эти статусы. В этой ветке я вам вообще ничего не писал, я общался с другим человеком. Вы ведете себя неадекватно.

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

Это для вас почему-то является сложным понять, что дело не в самом статусе, а в сопутствующих данных. И модератор и пользователь могут переводить статью в Draft. Только для модератора нужно указывать причину отклонения, а для пользователя нет. И соответственно возвращать ошибку валидации с кодом 400, если модератор не заполнил причину.

И вот если это делать с REST, то получится куча if в коде и God-object в данных. Несколько раз уже объяснил разными словами, а вы все равно переводите разговор на другую тему. Не понимаете, напишите реализацию, я вам ткну пальцем в нужное место в коде. Не хотите понимать, так и скажите, не надо вместо этого пытаться принизить собеседника.

Ну вы сами такой дизайн придумали. Можно немного лирики?

— Зачем у вас верёвочная лестница на стене?
— Доставать еду из холодильника!
— А при чём тут холодильник?
— Так он к потолку прибит!
— А зачем?!
— Молчи, пёс! Таковы бизнес-требования! :)

Ну ок. И модератор и пользователь могут переводить статью в Draft. Т.е. по сути, делают абсолютно одно и то же действие на ресурсом, с одним и тем же результатом. Только для пользователя это его статья, для модератора -- чужая.

Опять же, не вижу проблем. У вас будут if-ы, что в случае с RPC, что в случае с REST. Только с REST это будет очевидное действие с очевидным результатом. И правила легко назначать делкаративно. В случае же RPC, это два разных действия, и действий надо будет плодить на каждый чих, что не избавляет от потребности выполнять проверки. Иначе модератор может перевести статью в черновики пользовательским методом, а пользователь модераторовским. Почему нет?

Я не вижу проблемы God-object. Если ресурсы контекста "статья" для модератора и пользователя разительно отличаются, это будут разные представления, со своими атрибутами и глаголами. Если отличия незначительные, то достаточно правил валидации. Которые, повторюсь, никуда не деваются и для RPC.

Зачем у вас верёвочная лестница на стене?

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

У вас будут if-ы, что в случае с RPC, что в случае с REST.

Нет, не будут. Я уже написал реализацию для RPC, там нет никаких if для reason.

и действий надо будет плодить на каждый чих

Действий будет ровно столько, сколько описано в бизнес-требованиях.

Почему нет?

Потому что всё API модератора защищено одной проверкой на роль модератора, и она делается фреймворком. Аналогично для пользователя. Мне не надо делать эти проверки вручную в коде для каждого статуса каждой сущности.

Я не вижу проблемы God-object.

Я уже несколько раз ее описал, но вы это игнорируете. Сейчас у вас в ресурсе '/article/1/state' есть свойство reason, завтра бизнес вас попросит сделать специальный статус PUBLISHED_READONLY, который отключает комменты у статьи на несколько дней, и вы добавите еще свойство readonlyDays. Доступ пользователя и модератора тут ни при чем.

Давайте уже заканчивать. У меня в ресурсе есть поле reason, потому что это ваши требования.

Я работаю с прозрачным состоянием, вы работаете по методу чёрного ящика. И у меня не бывает проблем с этим. Никаких god-обжектов у меня не получаются. Описанные вами сложности начинают складываться из-за дизайна, который вы изначально сверстали под RPC.

завтра бизнес вас попросит сделать специальный статус PUBLISHED_READONLY, который отключает комменты у статьи на несколько дней, и вы добавите еще свойство readonlyDays

Так и вы добавите. Оно что, с потолка возьмётся? У вас появится метод /article/1/changeToReadonly, а у меня ничего не поменяется, если добавляется только новый статус, то контракт даже не меняется.

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

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

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

Я уже несколько раз на это ответил, начиная прямо с первого коммента этой ветки - хотите увидеть, делайте реализацию, я вам ткну пальцем. Не хотите, не надо разводить демагогию.

а у меня ничего не поменяется

Это ложь, у вас добавится третье свойство readonlyDays в ресурс '/article/1/state' рядом с reason.

Так и вы добавите. Оно что, с потолка возьмётся?

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

У меня появится метод /article/1/changeToReadonly , но он будет принимать небольшую структуру ReadOnlyParams, которая никак не зависит от DeclineParams, с одним полем readonlyDays. На него я повешу атрибуты валидации Required и Positive без всяких условий. И в Article или в других данных оно мешаться не будет.

А если завтра бизнес попросить добавить чекбокс "Уведомить автора", я добавлю свойство notifyAuthor в структуру ReadOnlyParams, не меняя структуру Article. И если он скажет, чтобы это действие было доступно только топ-модераторам, я добавлю проверку роли средствами фреймворка при обращении к этому эндпойнту.

Как разрешить в вашем варианте установку статуса PublishedReadOnly только для топ-модераторов, Draft с причиной для модераторов, а Draft без причины только для автора, мне даже представлять не хочется.

Это ложь, у вас добавится третье свойство readonlyDays в ресурс '/article/1/state' рядом с reason.

Если в бизнес-состоянии появится такое поле, оно появится и в соответствующем ресурсе. При чём readonlyDays уместно для RPC. Для REST это будет поле с датой/временем, до которой работает режим ReadOnly, и оно будет полностью соответствовать реальности, а не хрен знает во что там readonlyDays превратиться по итогу и к чему приведёт, ага-ага, читайте свой талмуд с мануалами.

Я не говорил, что я не буду добавлять, я говорил, что у меня не будет появляться God-object со всеми этими полями.

Это часть состояния, при чём тут God-object? Никаких других атрибутов, которые не относятся к состоянию там не появится.

А если завтра бизнес попросить добавить чекбокс "Уведомить автора", я добавлю свойство notifyAuthor в структуру ReadOnlyParams, не меняя структуру Article.

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

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

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

Как разрешить в вашем варианте установку статуса PublishedReadOnly только для топ-модераторов, Draft с причиной для модераторов, а Draft без причины только для автора, мне даже представлять не хочется.

Потому что возможно вы не работали со сложными системами, когда всё упирается не в действия, а в управление состоянием. В мобилке может быть упрощённый интерфейс, в админке развесистый, логика UI может быть вычурной и позволять пользователю сделать одно и то же самыми разными путями. Буду ли я делать на каждый такой чих отдельный метод? Я в своём уме, я берегу своё время и время коллег -- конечно же нет!

А может ли юзер привести систему в то или иное состояние с учётом контекста, за это должны отвечать бизнес-правила. Меня крайне волнует, чтобы в результате любого действия система была консистентна, и отвечала всем заявленным правилам. Сидеть разгребать тысячи разных действий? Увольте, это не по-взрослому. Есть правила бизнеса, именно они отвечают за систему. А разработчики UI не должны ходить и клянчить метод на каждую кнопку, это тоже колхоз, а не разработка.

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

Для REST это будет поле с датой/временем, до которой работает режим ReadOnly

Разрешать клиенту самому рассчитывать дату, серьезно?) Может еще и баланс счета после транзакции будем рассчитывать на клиенте?

а не хрен знает во что там readonlyDays превратиться по итогу

Я как программист API точно знаю, во что оно превратится. Если вы поддерживаете API, в коде которого не разбираетесь, проблема именно в этом, а не в RPC.

проверки должны работать не на ендпоинтах, а в ядре, и обойти это никак нельзя

Ага, а потом появляется cron job, который вызывает функциональность ядра, и ничего не работает, потому что действие разрешено только пользователю "Директор", а в кроне текущего пользователя нет. Дальше начинаются танцы с бубнами, чтобы это обойти.

Добавлю отдельный POST с созданием уведомления. Ваш метод начинает решать много не связанных задач

Почему это задача "Уведомить автора о переводе статьи в read-only" не связана с задачей "Перевести статью в read-only"?

У вас клиент еще и решает когда письма отправлять?) Захотел отправил два, захотел вообще не отправил, пофиг что в бизнес-требованиях написано "Отправить одно письмо". Нет, такая бизнес-логика нам не нужна.

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

Я могу сказать про вас то же самое. Считаем, что я прав? Или все-таки это неконструктивно?
Если в RPC это делается легко, а в REST сложно, то причина именно в следовании REST, а не в том, с какими системами я работал.

Сидеть разгребать тысячи разных действий это не по-взрослому. Есть правила бизнеса, они отвечают за систему.

Вас не смущает, что это у бизнеса есть тысячи разных действий, и они из требований никуда не денутся независимо от реализации?

Это мешанина из сотен, тысячей методов, в которых потом чёрт ногу сломит

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

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

- Я не вижу проблемы God-object.
- Она заключается вот в этом
- При чём тут God-object?

Понятно, вы решили скатиться в троллинг.
Дальнейшая дискуссия мне неинтересна. Приведу только определение, похоже вы с ним не знакомы.

Божественный объект
"Божественный объект (англ. God object) — антипаттерн объектно-ориентированного программирования, описывающий объект, который хранит в себе «слишком много» или делает «слишком много»"

Состояние, которая хранит в себе слишком много, называется God-object. Да, несмотря на то, что это состояние.

Разрешать клиенту самому рассчитывать дату, серьезно?) Может еще и баланс счета после транзакции будем рассчитывать на клиенте?

Вообще не понял, какие проблемы с расчетом даты и времени. Что за сакральные фобии и страхи?

Я как программист API точно знаю, во что оно превратится. Если вы поддерживаете API, в коде которого не разбираетесь, проблема именно в этом, а не в RPC.

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

Ага, а потом появляется cron job, который вызывает функциональность ядра, и ничего не работает, потому что действие разрешено только пользователю "Директор", а в кроне текущего пользователя нет. Дальше начинаются танцы с бубнами, чтобы это обойти.

Какие ещё крон джобы, это байки из мира поделок на коленке и пет проджектов? :) Не припомню никаких танцев с бубнами, как и каких-либо причин запускать какие-то там крон джобы. Задачи планируются в распределённом кластере расписания, оно работает на политиках, которые легко и гибко настраиваются, так как это те же бизнес-правила.

Если в RPC это делается легко, а в REST сложно, то причина именно в следовании REST, а не в том, с какими системами я работал.

Не верно. "Сложно" начинать делать REST, тем кто кроме RPC ничего никогда не делал, и пока не может выйти за границы мышления action-based логики. Ну это как бояться матана и оставаться в уютной эвклидовой геометрии, а этого вполне достаточно чтоб плитку класть и обои клеить :)

Вас не смущает, что это у бизнеса есть тысячи разных действий, и они из требований никуда не денутся независимо от реализации?

Нет, не смущает. Меня смущает обезьяний труд, который вы настойчиво защищаете.

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

Эти же принципы управления состоянием прекрасно, просто чудесно кладутся на GQL. Даже более скажу, RPC подход там весьма хреново себя чувствует.

Состояние, которая хранит в себе слишком много, называется God-object. Да, несмотря на то, что это состояние.

Не в кассу, и вообще это из другой оперы. Что в к нему привязались, до сих пор понять не могу.

Если вы единственный носитель знаний как там ваш чёрный ящик работает

Вы вообще неспособны представить, что под выражением "Я как программист API" подразумевается команда разработчиков сервиса?

"Сложно" начинать делать REST, тем
и пока не может выйти за границы мышления action-based логики

Да при чем тут я-то?) Что за переход на личности 2 раза подряд на одну и ту же тему?
Если в вашем подходе это просто, покажите как. Там дел блин на 3 строчки. Столько это занимает с RPC. Не можете, значит прекратите оскорбления, я не являюсь причиной вашего незнания или сложности вашего подхода.

Меня смущает обезьяний труд

"Обезьяний труд" пока существует только у вас на словах, никакую реализацию для их подтверждения вы не привели.

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

Что вы к нему привязались, до сих пор понять не могу.

Потому что обезьяний труд по поддержке этого God-object мне не нужен.

Даже более скажу, RPC подход там весьма хреново себя чувствует.

А, так вы даже не знаете, что такое RPC. Мутации с названиями и аргументами - это процедуры в чистом виде. А их вызов с клиента это remote procedure call, просто по определению.
Вот цитата от одного из ведущих разработчиков GraphQL.

https://www.apollographql.com/blog/graphql/basics/designing-graphql-mutations/
"This mutation is more like an RPC call then a simple CRUD action on a data type."

Советую вам задуматься об уровне своих знаний.

- Вот реализация RPC, попробуйте сделать аналогичную реализацию REST
- Она будет такая же
- Покажите
- Не покажу
Вообще не понял, какие проблемы с расчетом даты и времени
Какие ещё крон джобы
Не в кассу, и вообще это из другой оперы

Ну я и говорю, троллинг.

данные, которые в REST должны быть подмножеством полей ресурса.

Нет в REST (rest api, если быть точным) такого ограничения. От куда всего его выдумывают?
Передавайте что хотите. Тут исключительно вопрос удобства реализации, тестирования, документирования и поддержки всего этого дела.
В общем случае просто получается удобнее, если то что возвращает GET можно потом отправить в PATCH.
И фреймворки писать удобнее, когда у нас есть супер жёсткие ограничения. Можно тогда свести весь код приложения к минимуму - указал фреймворку на таблицу в базе данных, а остальное он всё сам сделает.

От куда всего его выдумывают?

Я предполагаю, потому что REST основан на передаче representational state ресурсов. В исходном документе написано так:

https://ics.uci.edu/~fielding/pubs/dissertation/fielding_dissertation.pdf
"REST components perform actions on a resource by using a representation to capture the current or intended state of that resource and transferring that representation between components."

Соответственно, если данные не являются resource state, который потом можно прочитать, то передавать их неправильно.

Не думаю, что это определение запрещает передать, что-то дополнительное, что не является прямым отражением на поля ресурса в базе данных или ещё где хранится ресурсв. Репрезентация может иметь любой вид как по форме так и по содержанию. На то она и репрезентация сущности, а не сама сущность.
Например у ресурса может быть поле published с датой публикации. Но мы не хотим давать клиентам менять его напрямую. Для этого клиент может передавать "виртуальное" поле is_published: bool, а уже сервер будет в зависимости от него менять значение поля published.

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

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

Там сказано "state of that resource", state ресурса для клиента это то, что клиент может прочитать.

Там это много где упоминается.

An origin server maintains a mapping from resource identifiers to the set of representations corresponding to each resource. A resource is therefore manipulated by transferring representations through the generic interface defined by the resource identifier.

For example, remote authoring of a resource requires that the author send a representation to the server, thus establishing a value for that resource that can be retrieved by later requests.

REST was used to define the term resource for the URI standard, as well as the overall semantics of the generic interface for manipulating resources via their representations.

Вот еще с другого ресурса.
https://restfulapi.net/
The resources should have uniform representations in the server response. API consumers should use these representations to modify the resource state in the server.

С учётом того, что репрезентация ресурса может быть совершенно разной, то я не думаю, что Филдинг закладывал в REST ограничение на то, что может возвращать и принимать сервер.
В самом класическом виде сервер может возвращать ресурс в виде html страницы, а принимать его в виде html формы. Так же ресурс может возвращаться сервером как картинка (например график), а создаваться передачей json-а со списком точек.
Это всё настолько "особенности реализации", что не стоят упоминания в фундаментальных ограничениях архитектуры (даже не приложения или протокола), которые разработал Филдинг.

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

HTML и картинка это формы представления данных, то есть как раз representations, состояние ресурса в них одинаковое. Можно представить, что ресурс с графиком возвращает картинку с "Accept: image/jpeg" и исходные точки с "Accept: application/json".

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

Ну а вот правила обработки REST API на 60 строк, где не требуется копипаста логики контроллера:

class Article extends Entity {
    @attr.public.author title = ''
    @attr.public.author body = ''
    @attr.author.porter concern = null as null | Concern
    @attr.public.author public_ready = false
    @attr.author.system public_block = [] as readonly string[]
    @attr.public.system public = false
    @attr.system.system public_logged = true
}

class Concern extends Entity {
    @attr.public.author message = ''
    @attr.system.system mailed = false
}

Fund( Article ).effects({
    
    public_block_calc: article => article.public_block = [
        ... this.public_ready ? [] : [ 'ready_awaiting' ],
        ... this.title.trim().length < 10 ? [ 'title_too_short' ] : [],
        ... this.body.trim().length < 128 ? [ 'body_too_short' ] : [],
        ... this.concern?.message ? [ 'concern_exists' ] : [],
    ],
    
    public_calc: article => {
        if( article.public === ( article.public = this.public_ready && !this.public_block.length ) ) return
        article.public_logged = false
    },
    
    public_log: article => {
        if( article.public_logged !== false ) return
        if( article.public ) {
            Log.rise({
                message: 'article_published',
                article: article.id,
                author: article.author.id,
            })
        } else if( article.concern ) {
            Log.rise({
                message: 'article_declined',
                article: article.id,
                moderator: article.concern.author.id,
                reason: article.concern.message,
            })
        }
        article.public_logged = true
    },
    
    concern_mail: article => {
        if( article.concern?.mailed !== false ) return
        Mail.send({
            target: article.author.mail,
            template: 'article_declined',
            article: article.id,
            title: article.title,
            moderator: article.concern.user.id,
            reason: article.concern.message,
        })
        article.concern.mailed = true
    },
    
})

Ваш код работает не так, как мой.

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

Нет отправки кода 400 при ошибках валидации (для сообщений title_too_short / body_too_short).

Отправка email должна происходить только после успешного сохранения данных в базу. У вас она происходит во время вычисления эффектов, то есть видимо до сохранения сущности Article.

Log.rise() должен сохранять запись в таблицу базы данных article_log в одной транзакции с сохранением article. Неясно, как у вас это гарантируется, похоже что никак.

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

При отклонении модератором статья не скрывается в черновики, а остается в publiс_ready. Если признак черновика показывает свойство public, то тогда непонятно, что означает ситуация 'publiс_ready = false'. Бизнес использует термины "В черновике" и "Опубликована", и хочет видеть их хотя бы в интерфейсе, поэтому нужен критерий как мапить эти свойства на эти термины.

if (article.public === (article.public = a && b)) return

Цель сравнения свойства с самим собой непонятна и выглядит как ошибка.

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

60 строк, где не требуется копипаста логики контроллера

Сначала реализуйте правильно бизнес-требования, а потом будем строки считать. Проверим, как у вас контроллер отправляет 400, и как база сохраняет 2 строки в транзакции.
Как вы верно заметили, код в контроллере очень похож, и легко автоматизируется в виде процессора, который автоматически создает input dto, делает валидацию, вызывает нужный сервис и возвращает результат. Но это выходит за рамки обсуждения. Я его написал, чтобы было понятно, как выглядит веб-интерфейс.

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

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

А если вы не в состоянии прочитать и осознать тривиальный код, то запишитесь на курсы войти-в-айти. Кроме "В черновике" и "Опубликована" есть ещё статус "На проверке" для статей, переведённых на премодерацию.

Вы, кажется, совсем берега попутали.

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

Мне на хрен не сдались ваши ошибки валидации

Ради бога, только тогда не надо в ответе на мой комментарий выдавать ваш код за аналог приведенного примера на REST.
В примере я указал те действия, которые требует бизнес во многих задачах - валидация входных данных, сохранение в несколько таблиц в транзакции, отправка уведомлений или запросов в сторонние системы только после успешного сохранения. Подходы, которые не позволяют это делать, меня не интересуют. Я не знаю, зачем вы мне про них пишете.

Попробуйте реализовать это поведение в виде REST.

Побробовал. Могу в 1 строчку реализовать REST (просто унаследовавшись от рест-контролера из коробки). Для этого нужно всего лишь создать отдельную таблицу для колонки is_published (нормализовать таблицу). Тогда этотот флаг будет меняться просто ресурсом PATCH /publish.

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

Что касается вашего примера, то вы получается реализовали т.с. "нормализацию" с помощью кода.

  1. Приведите код пожалуйста. Я свой код привел.

  2. Выделение is_published вместо status в данном случае это ни разу не нормализация.

  3. Смысл не в том, чтобы просто поменять флаг. Нужно реализовать всё поведение, которое ожидает бизнес, а не выборочно. Сохранение в обе таблицы в одной транзакции, отправка email только после успешного сохранения, валидация заполненности reason для модератора, валидация заполненности текста для опубликованной статьи.
    Я сильно сомневаюсь, что это можно сделать одной строчкой в рамках общего фреймворка.

Вот такой вопрос. Какую ошибку выдавать если запрос верный но за вашим сервисом еще один и он помер. 500 как бы странно, ваш сервис то жив.

Нужно отдавать 500. Ничего странного, для клиента нет никакого "вашего" и "не вашего" сервиса. Есть бекенд. А бекенд несёт ответственность за обработку поступающих запросов. Если валидный запрос он не может обработать по любым причинам: не доступен сервис, БД, диск переполнен, память кончилась, просто дефект, нужно возвращать 500, ибо клиент на это никак повлиять не может.

Ещё нужно понимать, что ошибки 500 являются транзиентными, а ошибки 4xx нет (кроме 408). Это означает, что клиент имеет право повторить свой запрос в ответ на 500, ожидая, что ошибка уйдёт. Но не имеет право этого делать в ответ на ошибки 4xx. Ибо, если запрос некорректный, ничего не изменится при повторении запроса. А вот БД, внутренний сервис, могут и ожить :)

Почему не 502 или 503?

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

404 Not Found это ответ вебсервера, а не рест сервиса. код 201 внёс путанницу в этот процесс.

Если сервис работает корректно то код ответа веб сервера должен быть 200

Есть REST API, это манифест который описывает как надо проектировать API, а сами API мы разделяем на REST и RESTful, они отличаются тем что второй соответствует всем канонам манифеста.

Очень важно понимать что тело запроса шифруется (при https) и из за этой причины очень часто используется post, например при о правке запроса авторизации.

Еще одним очень важным фактором является то для какого клиента бы делаем API, если это фронт то лучше вернуть 404, если другой бэк то пустой массив, если наши клиент это сторонние системы типа ibm lotus или 1с то лучше реализовать API на get и post миннуя другие методы.

Так что если делаем для масс то лучше придерживаться манифесту и сделать RESTful API в противном случае исходить из клиента, функционал и требований к безопасности.

Очень важно понимать что тело запроса шифруется (при https) и из за этой причины очень часто используется post, например при о правке запроса авторизации.

Так ведь заголовки тоже шифруются. В чём же тут преимущество POST?

Так вы забыли еще написать что и куку шифруются.
Только я не понимаю в чем суть вашего высказывания?
Через POST можно тело запроса в форматах, чаще всего, в текущих реалиях это json, но я видел системы которые работают через xml.
POST удобен для тестирования API из разных инструментов, например в postman, если хочешь поделится запросом то нужно всего лишь отправить json.
POST это сложная структура данных, есть запросы авторизации где кроме имени пользователя и пароля передаются дополнительные данные в виде даты-времени, региона, версия приложения и ОС ид.
Давай теперь header - это структура ключ-значение.
Можно передавать все вышеописанные поля? да можно, но попробуй поделится запросом, curl? "очень удобно".
Как заполнить заголовки перед отправкой запроса в хедры? циклом с проверкой, очень удобно.
Так а как отправить сложный запрос авторизации? так объект преврати ключ значение, очень удобно.
Есть решение чтобы так не делать? да, есть, например digest auth или jwt чтобы решить данную проблему кодируют объект в base64 и отправляют в виде заголовка, так а что там отправляется? да можно декодировать и посмотреть!

А почему так делают амазон или гугл в своих апи? наверное дураки.

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

Насколько оправдано цепляться за REST, как за "золотой молоток", а не использовать иные средства асинхронного взаимодействия, более адаптированные к такому использованию, чем REST?

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

А как можно "мониторить" задачу через любой другой сервер, если не накладывать такое явное ограничение к архитектуре, как общий кластер СУБД?

Так же Вы допускаете отказ от stateless, несмотря на то, что sateless - обязательное требование к REST по Филдингу. Так может это уже не REST, а уже какой-то nin-jin-TP?

Вы похоже не понимаете, что stateless не про отсутствие данных на сервере, а про отсутствие временного состояния, привязанного к конкретному клиенту. Это позволяет заходить разными клиентами на разные узлы и иметь один и тот же результат.

Stateless, дословно, "без сохранения состояния". А Вы предлагаете клиентом запрашивать СОСТОЯНИЕ ранее преданного запроса.

Это позволяет заходить разными клиентами на разные узлы и иметь один и тот же результат.

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

Если более подробно, то нарушение принципа stateless обозначает, что все узлы должны иметь доступ не только к общим данным системы, а так же к временным данным состояния каждого клиента. И если общие данные системы без проблем могут обновляться даже по низкоскоростному радиоканалу, то передавать по этому каналу еще и результаты запросов КАЖДОГО клиента к КАЖДОМУ серверу, обычно, даже технически не реально. При соблюдении stateless, ответ от сервера получите из его локальной БД, ожидая завершения запроса по ней. И никто, кроме этого сервера и только в рамках одного запроса, не будет ничего знать о статусе его выполнения.

Не запроса, а задачи - созданного в рест-сервисе ресурса.

Да хоть горшком назовите, от этого дело не меняется )

Я же привел примеры!

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

Обычно это сводится к передаче аутентификационных данных пользователя в каждом запросе. Но в целом это можно обойти, если сделать сессии обычным ресурсом, который доступен на всех серверах кластера - например хранить их в Redis или БД, а не использовать файловые сессии PHP.

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

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

если сделать сессии обычным ресурсом, который доступен на всех серверах кластера

А тут проблемы с каналами связи. Я подробно писал об этом выше https://habr.com/ru/articles/770226/comments/#comment_26124488

"если общие данные системы без проблем могут обновляться даже по
низкоскоростному радиоканалу, то передавать по этому каналу еще и
результаты запросов КАЖДОГО клиента к КАЖДОМУ серверу, обычно, даже
технически не реально"

Например, мой тарификатор грузовых железнодорожных перевозок хранит все тарифное руководство в оперативке

Неважно, как у вас выглядит хранилище, важно что вы можете одним запросом поменять данные на новые, а другим прочитать эти новые данные. Это не stateless для данных, но принципам REST это не противоречит, потому что он не требует для данных (т.е. для состояния ресурсов) быть stateless. Это противоречит самой идее наличия ресурсов с состоянием.

Отличие БД от оперативной памяти называется persistence, со stateless это не связано.

по этому каналу еще и результаты запросов КАЖДОГО клиента к КАЖДОМУ серверу даже технически не реально

Не надо передавать результаты каждому серверу, достаточно если все сервера, принимающие REST-запросы, будут подключаться к одной общей базе данных (или другому хранилищу), в которой эти результаты хранятся.

Балансировщик на 2-3 веб-сервера, которые работают с одной БД это более чем реально. Вот stateless это как раз о том, чтобы любой из этих серверов мог одинаково обработать запрос.

Если клиент может отправить на любую соту запрос "хочу сделать звонок на этот номер", и он будет выполнен одинаково, то это stateless. А если по-разному, то stateful.

вы можете одним запросом поменять данные на новые, а другим прочитать эти новые данные

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

"никакие запросы к нему от клиентов его БД не модифицируют"

Не надо передавать результаты каждому серверу, достаточно если все
сервера, принимающие REST-запросы, будут подключаться к одной общей базе данных (или другому хранилищу), в которой эти результаты хранятся.

Вы вообще представляете себе, что такое низкоскоростной радиоканал? А его стабильность? Хотя бы по спутниковому каналу с пингом в секунду пробовали сервис напрямую к БД подключать?

к одной общей базе данных

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

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

я явно указал, что такого быть не может
никакие запросы его БД не модифицируют

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

Вообще говоря, я написал "Сервис, который не хранит данных". Я не понимаю, что вы хотите доказать примером сервиса, который "хранит данные в оперативке".

Вы вообще представляете себе, что такое низкоскоростной радиоканал?

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

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

Добавлю.

- Сервис, который не хранит данных, не имеет большого смысла, поэтому к данным понятие stateless относиться не может.
- Это неверно. Например, мой тарификатор грузовых железнодорожных перевозок хранит данные в оперативке

Данные это по определению state, поэтому stateless они быть не могут. Поэтому можно сделать вывод, что stateless означает что-то другое.

Данные это по определению state, поэтому stateless они быть не могут. Поэтому можно сделать вывод, что stateless означает что-то другое.

State в stateless обозначает состояние КЛИЕНТА и его данных с точки зрения сервера. Если действия сервера не зависят от состояния клиента - то это и есть stateless. А если сервер должен знать состояние клиента (ожидает он выполнения предыдущего запроса или не ожидает, забрал он уже эти результаты или нет, посылал он какие-то запросы раньше или не посылал) - это уже как раз совсем не stateless.

Stateless в REST означает состояние сессии, а не любых данных. Оно относится к протоколу взаимодействия, а не к хранению данных.

Естественно. И именно из этого следует, что сервер не должен ничего знать о состоянии клиента: "ожидает он выполнения предыдущего запроса или не ожидает, забрал он уже эти результаты или нет, посылал он какие-то запросы раньше или не посылал"

Изначально вы говорили про любые данные, а не про состояние клиента: "первым запросом мы инициируем что-то, а последующими запрашиваем его состояние". Я возражал на это утверждение. Когда "что-то" это например заказ, то никакого противоречия со stateless в REST тут нет.

Изначально я говорил: "REST, по определению авторов, обязан быть stateless. Но асинхронные запросы, когда первым запросом мы инициируем что-то, а последующими запрашиваем его состояние, в парадигму stateless не вписываются. Более того, такой подход накладывает определенные ограничения на архитектуру решения и усложняет горизонтальное масштабирование."
Остальное лишь плод Вашей фантазии )))

Когда "что-то" это например заказ, то никакого противоречия со stateless в REST тут нет.

Только до тех пор, пока транзакция не продолжается после отправки сервером ответа клиенту.

Видимо вы действительно решили скатиться в троллинг. Ок, давайте напишу подробнее специально для вас. Я возражал на ваше утверждение со словами "инициируем что-то" и "в парадигму stateless не вписываются", то есть именно то, которое вы процитировали в кавычках. Тут нет ничего "остального".

В парадигму stateless это прекрасно вписывается, потому что парадигма stateless относится не к созданию произвольного "чего-то". В REST разрешено создание ресурсов и дальнейшее получение их состояния, для этого используются POST и GET-запросы.

Только до тех пор, пока транзакция не продолжается после отправки сервером ответа клиенту.

В REST нет такого требования. Требование stateless к протоколу запросов не запрещает иметь фоновые процессы, которые меняют состояние ресурсов. Хоть транзакция, хоть обработка CSV-файла с прогресс-баром, хоть пайплайн с тестами.

В REST нет такого требования. Требование stateless к протоколу запросов не запрещает иметь фоновые процессы, которые меняют состояние ресурсов.

Вас переклинило. Попробуйте сами сформулировать, что такое stateless и statefull. И привести примеры. Тогда дойдет, что с Вашими определениями statefull вообще в природе не бывает )))

Я сформулировал это уже несколько раз, и примеры привел, но вы не хотите слушать. Ресурсы являются stateful, а протокол запроса к этим ресурсам stateless. Из чего следует, что "с моими определениями" stateful в природе бывает. Обе этих характеристики существуют одновременно, потому что применяются к разным вещам.

Stateless и satefull относятся именно к протоколу. Зачем сюда ресурсы приплели?

Из чего следует, что "с моими определениями" stateful в природе бывает.

То есть, Вы придумали свое собственное определение и с ним выносите тут мозги? )))

Ознакомьтесь с общепринятыми определениями: https://en.wikipedia.org/wiki/Stateless_protocol

Я использую определения, указанные автором REST в исходном документе.
https://ics.uci.edu/~fielding/pubs/dissertation/fielding_dissertation.pdf

The client-stateless-server style derives from client-server with the additional constraint that no session state is allowed on the server component.

Each request from client to server must contain all of the information necessary to understand the request, and cannot take advantage of any stored context on the server. Session state is kept entirely on the client.

Созданная транзакция это не session state и не context, это ресурс с состоянием. Stateless ресурсов в принципе не бывает, ни с асинхронной обработкой, ни с синхронной. Если вы читаете какие-то данные, это и есть state.

Вот здесь еще можете почитать.
REST API Design for Long-Running Tasks

cannot take advantage of any stored context on the server

А с чего Вы взяли, что сохранение на сервере инициированной предыдущим запросом транзакции не является контекстом? Мы же не о данных говорим, а именно о статусе: продолжается, зафиксирована или откачена!

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

Мы же не о данных говорим, а именно о статусе

Статус транзакции это тоже данные.

в некоторых случаях информация что заказ оплачен приходит от платежного провайдера

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

Как статус хранится на бэкенде это дело десятое, хоть в поле status таблицы order, хоть в виде отдельной таблицы платежей с полем order_id.

Бизнес хочет, чтобы в интерфейсе было прямо написано "Заказ оплачен" или "Заказ ожидает оплаты". Поэтому для интерфейса делается API с данными заказа, в REST это будет ресурс order с полем status. С точки зрения REST между таким заказом и транзакцией с id нет разницы, они оба ресурсы, состояние которых модифицируется в другом процессе. Равно как и нет разницы, если вы не храните статус вашей транзакции в какой-то таблице БД, а получаете его другими средствами.

Но вообще обычно бизнес также хочет, чтобы в БД было поле order.status в явном виде. Чтобы аналитики могли получить оплаченные и неоплаченные заказы в базе простым SQL-запросом, а не лазить по куче таблиц.

А я явно указал, что если модифицируются только данные в оперативной памяти, а не в БД, то это все равно stateful.

Запросы к тарификатору данные НЕ МОДИФИЦИРУЮТ. Нигде и никак. Если Вы вручную на калькуляторе посчитаете тариф по четырем книгам Тарифного руководства РЖД, хоть что-то в этих книгах от этого изменится?

И с чего Вы вообще взяли, что состояние клиента должно быть связано с состоянием БД?

Зачем мне это представлять? Мы говорим про веб-сервисы в стиле REST, которые между собой и с базой данных соединяются через нормальный интернет.

С чего Вы это взяли? Или Вы лично запрещаете на судах, железнодорожных станциях, шахтах или буровых пользоваться REST? )))

решается общим хранилищем для всех серверов

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

И с чего Вы вообще взяли, что состояние клиента должно быть связано с состоянием БД?

С чего вы взяли, что я это взял? Я прямым текстом написал "Неважно, как у вас выглядит хранилище". Это вы начали говорить про модификацию БД.

Запросы к тарификатору данные НЕ МОДИФИЦИРУЮТ.

Об этом я написал в следующем предложении. Даже если у вас сервис только для чтения данных, на принципы REST это НИКАК НЕ ВЛИЯЕТ.

К состояниям ресурсов понятие stateless относиться в принципе не может. Поэтому непонятно, почему вы утверждаете, что это в парадигму stateless не вписывается, если принципы REST и не утверждают, что оно должно вписываться. Парадигма stateless, изложенная в REST, относится к другим концепциям, а не к состоянию ресурсов. Ситуация "первым запросом мы инициируем что-то, а последующими запрашиваем его состояние" ей не противоречит. Можно иметь stateful ресурсы и stateless протокол запросов.

- Мы говорим про веб-сервисы в стиле REST, которые между собой и с базой данных соединяются через нормальный интернет.
- С чего Вы это взяли?

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

Или Вы лично запрещаете на судах, железнодорожных станциях, шахтах или буровых пользоваться REST?

Нет. Из моих слов это никак не следует.
Суда подключаются к веб-серверу REST по низкоскоростному радиоканалу, веб-сервер REST подключается к другим серверам сервиса, в том числе к базе данных, по нормальному интернету.

это и есть именно то ограничение на архитектуру решения и усложнение горизонтального масштабирования

Это все наверно правильно, только к тому, что означает stateless в REST это не имеет никакого отношения.

А Вы с совершенно непонятным упорством настаиваете на необходимости подключения любого сервиса к централизованной БД.

Я вам объясняю, что ваше утверждение "чтобы все узлы имели доступ, надо передавать результаты запросов КАЖДОГО клиента к КАЖДОМУ серверу" неверно. Не надо, для REST это не требуется. Возможно для вашего сервиса это требуется, а в других сервисах это решается общим хранилищем.

И делать централизованную БД необязательно. Можно иметь несколько БД и выбирать нужную в приложении по данным в запросе. Или можно перед API-серверами с разными БД поставить прокси, который по данным в запросе будет выбирать нужный сервер, и все веб-запросы направлять на прокси. И приложение и прокси можно масштабировать горизонтально, то есть запускать несколько инстансов.

Только это не относится именно к REST. Необходимость stateless-запросов это следствие горизонтального масштабирования - когда запущено 100 инстансов, и запросы между ними распределяются случайно балансировщиком, вы не можете полагаться на состояние одного сервера, следующий запрос от того же пользователя может уйти на другой сервер. Поэтому надо либо использовать общее хранилище, либо передавать все нужные данные в запросе.

если у вас сервис только для чтения данных, на принципы REST это НИКАК НЕ ВЛИЯЕТ.

Естественно. И я никак не пойму, почему эта простая истина так Вас возбудила )))

Ситуация "первым запросом мы инициируем что-то, а последующими запрашиваем его состояние" ей не противоречит.

Даже по определению противоречит: "A stateless protocol is a communication protocol in which the receiver must not retain session state from previous requests"

Суда подключаются к веб-серверу REST по низкоскоростному радиоканалу

Вы явно троллите или прикидываетесь дурачком. Я не верю, что кто-то в здравом уме и твердой памяти будет по СВ или УКВ запрашивать массивы данных по REST. Такие каналы используются только брокерами сообщений.

в других сервисах это решается общим хранилищем.

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

Более того, общее хранилище - большое зло, которого следует избегать в любой архитектуре. Например, в любой момент может лечь VPN канал между географически удаленными ЦОД. О чем клиенты и ведать не будут. Живой пример - ГИС ЖКХ, которая валится по несколько раз на день именно по этой причине. И пока они не уйдут со statefull на stateless - и будет так валится.

-- на принципы REST это НИКАК НЕ ВЛИЯЕТ.
-- И я никак не пойму, почему эта простая истина так Вас возбудила

Потому что вы сделали утверждение, что влияет. Я написал "Сервис, который не хранит данных, не имеет большого смысла, поэтому stateless не относится к состоянию ресурсов", вы написали "Это неверно, потому что мой сервис хранит данные в оперативке и не модифицирует БД".

Из того, что ваш сервис хранит данные в оперативке, не следует, что он стал stateless, так как данные это и есть state.
Из того, что ваш сервис хранит данные в оперативке, не следует, что это пример сервиса, который не хранит данных и имеет смысл.
Из того, что ваш сервис не модифицирует одно хранилище и использует другое хранилище, не следует, что что-то от этого принципиально поменялось.

Stateless в REST не относится к хранению данных, это просто факт. Поэтому я никак не пойму, почему вы постоянно пишете про хранение данных.

Даже по определению противоречит: "A stateless protocol is a communication protocol in which the receiver must not retain session state from previous requests"

Вот я вам и объясняю, что главное в этом определении это "session state" (состояние сессии), а не состояние любых ресурсов.

Вы явно троллите или прикидываетесь дурачком.

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

Я не верю, что кто-то в здравом уме и твердой памяти будет по СВ или УКВ запрашивать массивы данных по REST.

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

Вы искренне считаете, что REST может быть только с общим хранилищем?

Вы явно троллите или прикидываетесь дурачком. Я описал несколько подходов как обеспечивается stateless в REST, и не только в REST, в том числе с разными хранилищами.

Более того, общее хранилище - большое зло, которого следует избегать в любой архитектуре. Например, в любой момент может лечь VPN канал между географически удаленными ЦОД.

Я не понимаю, как одна общая база данных связана с разными ЦОД и падением канала между ними.
Если у вас база в одном ЦОД, а веб-приложения в другом, и вы подключаетесь к базе по VPN, это проблемы вашего подхода, а не REST.
Если у вас 2 базы в разных ЦОД, то в предыдущем комментарии я объяснил, как это связано со stateless.

Нет, общее хранилище данных (например база, кеш, amazon s3) используется примерно в 99% сервисов с API, и всё нормально работает.

данные это и есть state.

У Вас каша в голове. Перечитайте: "stateless протокол - это протокол передачи данных, который относит каждый запрос к независимой транзакции, которая не связана с предыдущим запросом". То есть каждое сообщение запроса может быть понято в изоляции от других запросов. Данные тут фигурируют только в виде хранения СОСТОЯНИЯ КЛИЕНТА на стороне сервера. Если сервер хранит, что клиент когда-то послал какой-то запрос и ожидает теперь завершения транзакции - это уже statefull протокол, а вовсе не stateless.

Вот я вам и говорю, что нормальные люди используют для этого нормальный интернет.

Спуститесь с небес на землю )))

Я не понимаю, как одна общая база данных связана с разными ЦОД и падением канала между ними.

Элементарно, Ватсон. Если клиент послал запрос на выполнение транзакции на сервис в одном ЦОД, после чего попытался запросить результат этой транзакции на сервисе в другом ЦОД, то при падении канала между этими ЦОД он получит в ответ фигу )))

общее хранилище данных (например база, кеш, amazon s3) используется примерно в 99% сервисов с API

Меня поражает Ваша наивность. В enterprise общие БД встречаются не чаще, чем медведи в подмосковье. Обычно они распределены по нескольким ЦОД и вполне работоспособны независимо.

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

Мне не надо это перечитывать, именно это я вам и пытаюсь объяснить. Вы неправильно понимаете, что это означает.

Если запрос GET /transactions/<id> возвращает данные транзакции с этим id для всех пользователей, у которых есть доступ, это значит, что он может быть понят независимо от предыдущих запросов этих пользователей. У одного был предыдущий запрос на создание, у другого не было, но на результат этого запроса это не влияет.

Данные тут фигурируют только в виде хранения СОСТОЯНИЯ КЛИЕНТА на стороне сервера.

А изначально вы говорили про любые данные: "первым запросом мы инициируем что-то (что угодно)". Вот я вам и объясняю, что если вы первым запросом создали заказ, а вторым его прочитали, то это не является нарушением требования stateless.

Спуститесь с небес на землю

У вас неправильное представление о небесах и землях. На моей текущей работе и на всех предыдущих не было никаких радиоканалов. Это у вас специфичный кейс, а не у меня.
Интернет для обмена данными используется гораздо чаще, чем низкоскоростные радиоканалы. Если вы считаете по-другому, то это вы витаете в облаках.

Если клиент послал запрос на выполнение транзакции на сервис в одном ЦОД, после чего попытался запросить результат этой транзакции на сервисе в другом ЦОД

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

2 комментария назад я уже объяснил, как сделать stateless с несколькими БД.
Если говорить более конкретно, то в данном случае можно возвращать идентификатор ЦОДа вместе с данными созданной транзакции (или косвенные данные, по которым его можно определить, например страну транзакции), и сделать это обязательным параметром при запросе состояния транзакции. Тогда сервис в любом ЦОД может сделать HTTP-редирект на нужный ЦОД. Это как раз то, что подразумевается в требовании "передавать все нужные данные в запросе".

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

Обычно они распределены по нескольким ЦОД и вполне работоспособны независимо.

Шардинг на несколько независимых баз требуется для очень крупных сервисов мирового уровня, не любой enterprise это сервис мирового уровня. Это и есть оставшийся 1%. Я в общем-то даже сомневаюсь, что на любые 100 сервисов с API обязательно будет 1 сервис с несколькими БД. А вот горизонтальное масштабирование веб-интерфейса встречается гораздо чаще, и там нужен stateless.

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

Нет, ресурс "История действий пользователя" не противоречит концепции stateless.
Вы неправильно понимаете, что означает stateless конкретно в REST. Я попытался объяснить, но вы не хотите слушать. Ну дело ваше.

А изначально вы говорили про любые данные: "первым запросом мы инициируем что-то (что угодно)". Вот я вам и объясняю, что если вы первым запросом создали заказ

Вы перепутали инициирование (начало транзакции), и создание (выполнение транзакции)

Это и есть оставшийся 1%.

По количеству юридических лиц, возможно, да. По капитализации - уже порядка 50%

По капитализации

Мое утверждение было "общее хранилище данных используется примерно в 99% сервисов с API", что подразумевает количество отдельных сервисов, а не их капитализацию.

Вы перепутали инициирование (начало транзакции), и создание (выполнение транзакции)

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

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

Если пользователь в своей сессии создал (инициировал) транзакцию, а менеджер организации в своей сессии видит ее в списке выполняющихся транзакций, значит создание транзакции от сессии не зависит. А следовательно в терминах REST запрос на ее создание является stateless. Дальше думайте сами.

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

А это уже вряд ли. Одно РЖД по количеству отдельных сервисов перекроет все сервисы, которые Вы знаете и вряд ли имеет хотя бы одну "общую БД". А таких enterprise - тысячи.

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

У Вас еще и для транзакции есть свое определение? )))

По общепринятому определению, транзакция - ACID. Она завершается либо откатом, либо фиксацией. И до завершения транзакции никакие промежуточные статусы, в общем случае, другим запросам не доступны.

Понятно, что в распределенных системах, по CAP теореме, приходится отказываться от CA в пользу AP и "целостностью в конечном итоге". Но транзакции при этом все равно остаются транзакциями, пусть и с двухфазной фиксацией.

А это уже вряд ли.

Извините, я лучше знаю, что я подразумевал в своем высказывании. У меня не было даже намека ни на какую капитализацию.

- И если произошел сбой, то вы получите заказ
- У Вас еще и для транзакции есть свое определение?

Вы теперь еще и мои слова перевираете? Промежуточные статусы в моем примере относятся к понятию заказ, а не транзакция.

Она завершается либо откатом, либо фиксацией. И до завершения транзакции никакие промежуточные статусы, в общем случае, другим запросам не доступны.
Мы же не о данных говорим, а именно о статусе: продолжается, зафиксирована или откачена

Вы сами себе противоречите.

По общепринятому определению, транзакция - ACID

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

Но транзакции при этом все равно остаются транзакциями

Если после создания вы возвращаете на клиент какой-то id, по которому клиент может отслеживать, выполнена она или нет, то в терминах REST это ресурс, и после получения id он считается созданным. Как вы называете это в терминах вашей предметной области (инициирование или как-то еще), для REST не имеет значения, в нем это называется "создание ресурса".

Мне неведомо, что вы называете транзакцией.

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

За тем раскланиваюсь.

Ха-ха) Нет, извините, информация о том, что вы называете транзакцией, не является базовым понятием.

Вы писали "Интересен идеологический вопрос", но видимо вам не нужен на него ответ, вам важнее троллинг и оскорбления.

информация о том, что вы называете транзакцией, не является базовым понятием

Если написано "транзакция - ACID", то двоякие толкования могут возникнуть только у того, кто в этом вообще не разбирается. )))

Вы написали "транзакция - ACID" после того, как высказали претензию, что у меня какое-то свое определение транзакции со статусами. Я вам объяснил, что до этого мне было неведомо, что вы под этим подразумеваете, поэтому претензия неправомерна. Мысли читать я не умею.

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

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

stateles относится к тому, что клиент не должен рассчитывать на то, что сервер помнит его предыдущие запросы. Каждый новый запрос клиента должен содержать достаточно информации, что бы сервер мог его обработать. К серверу, в общем случае, это тоже относится - он не должен рассчитывать на то, что клиент помнит предыдущие ответы, он просто возвращает то что попросил клиент в текущем запросе.
Такой подход как раз позволяет упростить масштабирование сервиса, т.к. нам не надо реплицировать "знание" о предыдущих запросах на все сервера, куда может прийти запрос клиента (вы сами привели этот довод на примере сотовых станций).

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

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

И вот тут уже видно отсутствие "состояния".

Хоть кто-то признал, что состояние все же хранится )

роутинг произойдёт на уровне DNS

Получили первое архитектурное ограничение: на одно имя в DNS обязаны иметь не более одного адреса и от балансировки на этом уровне вынуждены отказаться.

балансировщик в ДЦ по URL-у определит какая "шарда" хранит в себе информацию об этой задаче и отправит запрос туда

Получили второе архитектурное ограничение: балансировщик обязан иметь доступ ко всем "шардам"

А теперь представьте себе мобильного клиента и "сервера" с локальным WiFi, с радиоканалом на 9600 бод и входящим широковещательным каналом 10 мегабит со спутника. Локальные БД на серверах синхронизированы с сервером в офисе.

Вопрос. Допустимо ли в данном случае послать REST запрос на один сервер, а получить результат другим REST запросом его уже с другого?

А про ГИС ЖКХ слышали? У них SOAP, а не REST, но проблема та же самая. Запросы там только асинхронные. Посылаешь запрос, получаешь в ответ UUID, по которому потом можно будет получить результаты вторым запросом, когда они будут доступны. Выбрать ЦОД невозможно - все под одним доменным именем. На Камчатке народ выл. Запрос случайным образом уходил то во Магадан, то в Хабаровск. Все бы ничего, но Магадский ЦОД хронически отваливался от Хабаровского. Вот так и долбишься по несколько минут, получая "запрос не найден", пока не попадешь наконец-то на тот же ЦОД, которому послал первый запрос. Это нормально?

Вы немного про другое спрашиваете. Можно ли получить состояние от другого сервера или нет - это не имеет отношения к stateless.
Stateless исключительно про то, что клиент должен в своём запросе передать всю необходимую информацию, что бы сервер мог его обработать.
Вот пример общения которое рассчитано на наличие стейта между клиентом и сервером:
Q: Сколько продуктовых магазино в Магадане?
A: 543
Q: А сколько обувных?
A: 100

Видите где тут стейт? Второй запрос не содержит "фильтра" по городам (в Магадане). Но т.к. сервер помнит стейт, то отвечает правильно.

Сделать такое не просто, если у нас больше одного сервера. Надо уметь определять в рамках какой сессии пришёл запрос от клиента. Надо как-то хранить "сессию" и как-то удалять её когда протухла. А клиент может создать 100500 сессий. Надо сделать эту сессию доступной на других серверах или обеспечить роутинг запроса так, что бы он попадал на те сервера где есть его сессия.

Примером "ужасного" протокола с состоянием является FTP - что бы попасть в какую-то папку надо по очереди послать команды на перемещение в следующую дочернюю папку. Сервер при этом помнит где находится клиент. После этого клиент может что-то делать в папке, в которую "зашёл". Если случиться разрыв связи, то клиенту придётся опять повторять всё что он делал до этого - пересещаться постепенно в нужную папку.

Вы немного про другое спрашиваете.

Нет, я именно про stateless и спрашиваю. Если строго stateless, то клиенту безразлично, какому серверу он посылает запрос. А серверу безразлично, к каким серверам до этого клиент обращался.

Вот пример общения

Ваш пример вообще явный, так как даже ссылка на предыдущий запрос отсутствует. Я же выше привёл более спорный пример общения с ГИС ЖКХ. Несмотря на то, что идентификатор предыдущего запроса передается сервером клиенту, ссылки на него в последующих запросах, на мой взгляд, нарушают принцип stateless. Ну и, самое главное, ярко демонстрируется ограничения на архитектуру такого решения. Ведь сервер должен создать сессию, выбрать для нее данные, сохранить их в промежуточную таблицу только для этого запроса этого клиента. После чего еще, в общем случае, с другого сервера, предоставить этому клиенту такие, по сути, временные данные. Да еще и зачистить их когда-то. А ведь это ровно тот же пример, что и в статье в разделе "Асинхронные запросы".

На мой взгляд, это откровенное костылестроение и использование REST не по назначению в качестве "золотого молотка". Если REST - то послал запрос и дождись пока сложный запрос выполнится и получишь результат. Если так не устраивает, то есть брокеры сообщений, которые сами доставят результат уже через PUSH, а не POLL.

Вы продолжаете какую-то другую задачу решать. Stateless не про то что любой сервер в кластере должен уметь обработать запрос клиента. Он только про то, что клиент должен передать в запросе всю информацию, которая нужна, что бы сервис мог обработать этот запрос.
Какой именно сервер будет обрабатывать запрос, и должн ли быть это любой сервер в кластере - это не относится к тому, что подразумевается под stateless в REST. Это другая задача, которая имеет разные решения. Начиная от полной, синхронной репликации базы продолжая шардированием с "умным" роутингом до нужной шарды и заканчивая такой реализацией где вообще не нужна база данных и любой сервер может обработать запрос клиента.

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

И какие ограничения в масштабировании накладывает stateless? По мне так наоборот становится проще - не надо решать пачку проблем для обеспечения работы в statefull режиме.
В stateless вообще же ничего не надо делать специального. Принял запрос, достал из него "инструкции", выполнил, вернул ответ и забыл.

Нельзя использовать гео-балансировку по DNS? Это что за такое странное требование к гео-распределённому сервису? Ок, если на один домен - один адрес, тогда можно сделать 100500 доменов.
А что не так с тем что балансировщик знает про все шарды? Если у вас в домене только один адрес, а серверов много, то очевидно придётся делать на этом адресе балансер по всем серверам.

И ещё раз повторю - эти проблемы не имеют ни какого отношения к stateless из REST. Stateless ни помогает их решить, ни делает их решение сложнее.

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

Нельзя использовать гео-балансировку по DNS?

Я начинаю уже уставать копировать одно и то же. "Выбрать ЦОД невозможно - все под одним доменным именем.". Значит какая там балансировка? Для Камчатки DNS, условно, случайным образом возвращает либо Магадан, либо Хабаровск. Изредка Москву, если его почему-то на спутниковый канал потянуло.

Кроме IP адресов и, очень похоже, текущей загрузки каналов, он ни хрена не знает.

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

В вашем примере всё таки есть синхронизация серверов

Она только для общих данных. Там не реально ни хранить временные результаты всех запросов на каждом сервере, ни передавать их через спутник. Вы представляете себе сколько таких мобильных клиентов на угольном разрезе, которые постоянно сыпят запросами?

"Асинхронные" запросы поверх rest ничем не отличаются от других. Мы создали какой-то ресурс в сервисе и позднее запрашиваем состояние этого ресурса. Обычная рутина для любого api.
А послать запрос и ждать ответ пока не вернёт - это вообще почти не рабочая схема в условиях работы с сетью, т.к. в любой момент связь может прерваться. А значит, что-бы это решить, придётся на клиенте запоминать какой-то уникальный идентификатор, что бы после реконекта запросить у сервера результат обработки своего запроса. Т.е. мы сделаем то же самое что и в "асинхронных" запросах.

"Асинхронные" запросы поверх rest ничем не отличаются от других.

Я пример два примера, в которых они очень даже отличаются. Но Вы их проигнорировали.

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

А Вас не смущает, что эту страницу Вы сейчас запросили именно таким образом? К тому же я явно указал, как поступаю в тех случаях, когда действительно возможны проблемы со связью. Вы хотите оспорить, что один PUSH менее эффективен чем многократный POLL?

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

А если пуш не дойдёт до клиента, что тогда делать?

Ничего. Если клиент не постучится к брокеру до протухания данных в топике - сам виноват.

требует более сложных решений на сервере и клиенте

Внутри этих решений - да. Но для разработчика применение кролика или кафки даже проще, чем такие асинхронные REST.

Я же выше привёл более спорный пример общения с ГИС ЖКХ
Вот так и долбишься по несколько минут, получая "запрос не найден", пока не попадешь наконец-то на тот же ЦОД, которому послал первый запрос.

Это говорит только о том, что ГИС ЖКХ криво сделали реализацию API, а не о том, что это происходит из-за использования REST или stateless асинхронных запросов. Это как раз не stateless, а stateful. Криво можно сделать и с RPC, и с брокерами сообщений.

Аналогично проявляется использование PHP-сессий с 2 веб-серверами и балансировщиком с рандомным выбором сервера.

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

Так проблема в рандомной балансировке + отсутствии или очень медленной репликации данных. Надо либо сделать умный роутинг запроса, что бы он направлялся в нужный сервер, а не в рандомный (всё уже придумано до нас и есть разные варианты). Либо обеспечить быструю репликацию базы данных на все сервера, но это несколько примитивный способ, который будет в пустую тратить ресурсы.

PS: SOAP - как раз таки stateful протокол, но к данной проблеме это ни как не относится. Точно такую же проблему можно получить с чем угодно, если не поавильно спроектировать архитектуру под условия гео-распределённого сервиса.

сделать умный роутинг запроса, что бы он направлялся в нужный сервер

Очень сильное ограничение на архитектуру. Фактически отказ от автоматической балансировки с деградацией производительности.

обеспечить быструю репликацию базы данных на все сервера

Уже изначально было сказано, что канал между ЦОД периодически падает. В принципе, не падающих каналов вообще не бывает в природе. Так что точно не вариант.

Вы еще про угольный разрез подумайте из первого примера. Что там было бы, если бы попытались использовать асинхронные REST запросы.

Как сделать не криво, я уже вам говорил. Вместо идентификатора вида "UUID" возвращается идентификатор вида "UUID тире номер ЦОДа", и балансировщик отправляет запрос на нужный ЦОД, а не рандомно.

Вообще, судя по тому, что они выбирают ЦОД рандомно сами, они предполагали, что данные будут доступны во всех ЦОДах. То есть можно предположить, что у них там мастер-база и реплики, и реплики отставали от мастера. От REST или не REST это не зависит.

"UUID тире номер ЦОДа", и балансировщик отправляет запрос на нужный ЦОД, а не рандомно.

Откуда тут балансировщик возьмется? Сейчас балансировщик живет в DNS выдавая разные IP на одинаковые запросы. А тут куда его ставить? Если в Хабаровском или Магаданском ЦОД, то смысл балансировки между каналами теряется. А они для Камчатки самое узкое место.

мастер-база и реплики

Я точно знаю, что они не настолько идиоты, чтобы реплицировать временные данные каждого запроса на все сервера.

чтобы реплицировать временные данные каждого запроса на все сервера

А как тогда один ЦОД может вернуть данные по UUID, который создан в другом ЦОД? Каждый раз опрашивает все ЦОДы компании "Нет ли у вас данных с вот таким UUID"?

Сейчас балансировщик живет в DNS выдавая разные IP на одинаковые запросы.

Обычно под балансировщиком понимается программа, которая контролируется IT-специалистами компании. В этом случае да, запрос сначала идет на сервер к балансировщику, а балансировщик его перенаправляет дальше, возможно на сервер в другом ЦОД.

то смысл балансировки между каналами теряется

А что именно балансируется, нагрузка на сервера компании или передача данных по интернету от клиента до сервера? Обычно под балансировкой понимают первое.

Если второе, то опять же при чем тут API на сервере. Если канал между ЦОДами упал, и данные есть в одном но нет в другом, то это решается только отключением балансировки между ЦОДами по IP, а не какой-то особой отправкой запроса на случайно выбранный IP.

А как тогда один ЦОД может вернуть данные по UUID, который создан в другом ЦОД?

UUID бывает не только V4. Поэтому на стороне связанных серверов по полю node не сложно определить, где оно было сформировано.

Обычно под балансировщиком понимается программа, которая контролируется IT-специалистами компании.

И что толку от такого балансировщика в распределенной системе? Вы всерьез предлагаете размещать его на центральном ЦОД в Москве и все запросы роутить туда?

А что именно балансируется, нагрузка на сервера компании или передача данных по интернету от клиента до сервера?

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

отключением балансировки между ЦОДами по IP

И все по одному каналу в Москву? Оригинально.

Давно же придумали как балансировать балансировщики - купленный IP адрес, выделенная AS и протокол BGP. Но "для бедных" можно и гео-DNS использовать, только TTL надо малый ставить.

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

Давно же придумали как балансировать балансировщики - купленный IP адрес, выделенная AS и протокол BGP.

Можно и так, но это вообще дела меняет в обсуждаемой ситуации.

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

А вот тут все сложно. С одной стороны, вроде бы правильно направлять все запросы в Хабаровск, если Магадан из Хабаровска не доступен. Но с другой стороны, если Магадан при этом доступен с Камчатки, то стоит ли ломиться и так в перегруженный канал? Данные в ГИС ЖКХ оперативностью не отличаются. Всяческие ТСЖ их туда раз в месяц загружают. И отставание БД даже на сутки не существенно.

на стороне связанных серверов по полю node не сложно определить, где оно было сформировано.

Ну так это и есть то, о чем я говорю. С идентификатором запроса передается идентификатор цода.

Только я подразумевал, что сервер редиректит клиента на другой цод, если у него нет данных, а не возвращает ошибку. Да, клиент в некоторых случаях (не во всех) потратит трафик на дополнительный запрос, зато получит данные. Магии не бывает. Это потребует меньше трафика, чем если он будет делать 100 запросов, пока не попадет на нужный цод.

И что толку от такого балансировщика в распределенной системе?

Толк в том, что он балансирует нагрузку, а не трафик.

Вы всерьез предлагаете размещать его на центральном ЦОД в Москве и все запросы роутить туда?

Это работает для многих распределенных сервисов. Вы же не думаете, что google.com имеет цоды в каждом городе каждой страны?

И необязательно запускать один инстанс, можно запустить несколько в разных городах. Их да, надо балансировать по DNS. Но они смогут перенаправить запрос куда надо, а не просто вернуть ошибку.

если HTTP

REST Web-API это тоже HTTP, он не может работать не так, как HTTP.

то REST такое не позволяет

Откуда вы это взяли? В описании REST ничего подобного нет. Там нет требований про IP и DNS. В требовании stateless он как раз наоборот требует, чтобы было возможно обратиться на географически любой сервер.

Вот статьи как устроена балансировка в Google Cloud. Там нет ничего о том, каким должно быть API на сервере.
https://cloud.google.com/load-balancing/docs/load-balancing-overview
https://cloud.google.com/architecture/global-load-balancing-architectures-for-dns-routing-policies

И все по одному каналу в Москву?

Нет. Поймите, если в одном цод данные есть, а в другом нет, то выбирать их случайно по IP невозможно, как бы вам ни не нравилось отсутствие автоматической балансировки между ними. И неважно, какое API на сервере в цоде, оно не сможет вернуть данные если их нет.

В первую очередь, естественно, магистральные каналы.

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

С идентификатором запроса передается идентификатор цода.

А вот это уже нет. Идентификатор хоста (или чаще пода) на ЦОД позволяет определить через него ЦОД, но не на стороне клиента.

Толк в том, что он балансирует нагрузку, а не трафик.

Это вообще разные балансировки. В подавляющем большинстве случаев, геораспределенные системы делают именно ради балансирования трафика и повышение SLA. Нагрузку можно балансировать даже в пределах одного ЦОД.

Откуда вы это взяли?

Оттуда, что REST не рендерит HTML страницы, как браузер, который сам отправится по ссылке в src по другому адресу для получения потока оттуда.

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

А что тут понимать, если я именно с этого и начинал? И именно реализацию REST, которая допускает такое, я и не воспринимаю, как stateless. Как только "асинхронными запросами" из статьи мы допускаем формирование временных данных для ответов на последующие запросы, мы накладываем указанное Вам ограничение на архитектуру решения.

Что изменится, если на сервере будет не REST?

Я вообще-то писал выше. "Если REST - то послал запрос и дождись пока сложный запрос выполнится и получишь результат. Если так не устраивает, то есть брокеры сообщений, которые сами доставят результат уже через PUSH, а не POLL."

Идентификатор хоста на ЦОД позволяет определить через него ЦОД, но не на стороне клиента.

Это несущественные детали. Главное чтобы сервер знал, куда перенаправить клиента. Я писал "или косвенные данные, по которым его можно определить".

Оттуда, что REST не рендерит HTML страницы, как браузер, который сам отправится по ссылке в src

Никакое API не рендерит HTML страницы, как браузер. Поэтому непонятно, почему вы выдаете это за недостаток REST.

Поведение "сам отправится по ссылке" задается через код 301 и заголовок Location. Любой нормальный HTTP-клиент, который обращается к REST API, нормально его обработает и сам перейдет по указанному адресу. Дело API отдать его если нужно. Браузер это один из HTTP-клиентов.

А что тут понимать, если я именно с этого и начинал?

То, что "отключение балансировки между ЦОДами по IP" это единственный возможный вариант решения, и высмеивать его словом "Оригинально" как будто есть другое решение некорректно. Вы похоже этого не понимаете, и считаете, что есть другой вариант решения с сохранением случайного выбора IP.

И именно реализацию REST, которая допускает такое, я и не воспринимаю

Я уже несколько раз написал, как можно с REST такое не допускать. А значит использование REST ни на что не влияет. Мне непонятно, почему вы это игнорируете и продолжаете твердить одно и то же.

Еще один вариант - сервер сам обращается по интернету к REST API в другом цоде. Внутренний канал между цодами может не работать, но интернет точно работает, раз запрос пришел. Тогда клиенту даже дополнительный трафик тратить не надо.

мы накладываем указанное Вам ограничение на архитектуру решения.

Ограничение "не использовать балансировку по IP" существует из-за падения канала между цодами, а не из-за REST. Отправлять запрос в цод, где нет данных, не имеет смысла с любым API. Я никак не пойму, почему вы думаете, что с другим API или с брокерами канал между цодами падать не будет, или что данные с упавшим каналом магически появятся во всех брокерах.

-- Что изменится, если на сервере будет не REST?
-- Я вообще-то писал выше. Если так не устраивает, то есть брокеры сообщений

Я вообще-то задал вопрос о деталях того, что вы писали выше.
"Есть брокеры сообщений" не является ответом на вопрос "Что изменится". Вот есть у нас брокеры на серверах в разных ЦОДах, а дальше что? Балансировка же случается до обращения к брокеру. Обратились не к тому брокеру, там нет данных, придется переподключаться к другому. По сравнению с REST ничего не изменилось.

есть брокеры сообщений, которые сами доставят результат

Еще раз задаю вопрос, который вы проигнорировали. Опишите пожалуйста детали схемы, как вы будете подключаться к одному из 2 брокеров в Хабаровске и Магадане с балансировкой трафика?

Вот есть у вас доменное имя broker.gis-gkh.com, оно рандомно разрешается в один из 2 IP. Вы подключились к домену, получили IP Хабаровска, создали запрос, а потом связь пропала. Вы повторно подключаетесь к этому домену, получили IP Магадана, и ждете ответ на запрос 3 дня пока связь между цодами починят. Как это исправляет проблему? Никак.

через код 301

А при чем тут перманентный редирект, да еще и помимо балансировщика? По стандарту клиент после этого должен будет уже только туда ломиться.

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

Конечно есть. Я его уже дважды описывал.

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

Давайте не будем тут рассматривать вариант кривых рук при настройке, например, ZooKeeper.

Вот есть у вас доменное имя broker.gis-gkh.com

Нет, у меня есть доменное имя bootsrap server. А уж к какому именно брокеру он направит клиента - его дело. Да, в этом случае, в период времени пока их оффсет в партиции не актуальный, Камчатскэнерго окажется прибит гвоздями к одному брокеру, а Камчатскводоканал и какие-то ДЭЗ/ТСЖ - к другому. Но балансировать загрузку на каналы это уже поможет. Как только клиент заберет все свои данные, его можно будет переключить на другого брокера. А если клиент не забирает данные, то и нагрузку на канал он не создает.

А при чем тут перманентный редирект, да еще и помимо балансировщика?

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

DNS выдал случайный IP, сервер на этом IP сказал "у меня сейчас нет этих данных, иди на тот сервер, вот ссылка", клиент обратился на тот сервер, получил данные. Всё, никаких проблем. Не нравится 301, используйте любой подходящий. Если сервер вместо этого выдает ошибку, причина не REST, а кривая реализация.

Конечно есть. Я его уже дважды описывал.

Нет. Всё, что вы сказали, это что есть некие брокеры, которые по вашему мнению как-то решат проблему отсутствующих данных при сохранении балансировки по IP и без дополнительного трафика от клиента к серверу. На мой вопрос "как именно" вы решили не отвечать.

Давайте не будем тут рассматривать вариант кривых рук при настройке, например, ZooKeeper.

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

окажется прибит гвоздями к одному брокеру
его можно будет переключить на другого брокера

Это и есть "отключение балансировки по IP", о котором я говорю. Клиент на протяжении некоторого времени отправляет все запросы в один брокер, а не в разные. То есть как я и сказал, это требуется с любым API.

"Отключение" не означает, что балансировки с REST вообще не будет. Это означает, что нет смысла произвольно выбирать сервер на каждый запрос, и нужна более сложная логика, при которой в некоторых случаях какое-то количество запросов от клиента будет идти на один и тот же сервер.

А уж к какому именно брокеру он направит клиента - его дело.

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

у меня есть доменное имя bootsrap server

Я мало работал с Кафкой и ZooKeeper, не знаю подробностей их работы. Bootstrap server находится в цоде? В каком из двух? Чем это отличается от подключения всегда к Хабаровску с REST?

Не нравится 301, используйте любой подходящий.

Это не мне нравится, это клиентам не понравится любое отклонение от стандартов.

Всё, что вы сказали, это что есть некие брокеры

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

Это и есть "отключение балансировки по IP", о котором я говорю. Клиент на протяжении некоторого времени отправляет все запросы в один брокер, а не в разные.

На какое-то время гвоздями к брокеру прибивается лишь один клиент. В связи с обилием клиентов - это уже не принципиально.

нет смысла произвольно выбирать сервер на каждый запрос

Вот как раз тот же ZooKeeper позволит выбирать брокера на каждый запрос, при условии, что данные по предыдущему запросу уже откоммичены клиентом.

Я мало работал с Кафкой и ZooKeeper, не знаю подробностей их работы. Bootstrap server находится в цоде?

Во-первых, как Кафка (начиная с версии 2.8) может работать без ZooKeeper (хранить конфигурацию кластера может и избранный вотумом брокер), так и ZooKeeper может обслуживать не только Кафку, так как он лишь хранит конфигурацию кластера. Во-вторых, bootstrap сервер отвечает только за выбор брокера для клиента. Если есть несколько bootstrap серверов, то они указываются списком в конфигурации клиентов. Так как трафик с самим bootstrap ничтожен, то несколько bootstrap серверов используются только в целях резервирования. Балансировать там нечего.

А конфигурация кластера может учитывать необходимость обрывать брокеру соединение с клиентом с его стороны, чтобы инициировать восстановление соединение с уже другим брокером через bootstrap сервер. На практике, клиент поддерживает постоянное TCP соединение с брокером, что позволяет получать данные в PUSH режиме.

Это не мне нравится, это клиентам не понравится любое отклонение от стандартов.

"301" это был пример, а не руководство к действию.

Вообще то я писал явно, что "применение кролика или кафки"

Вы писали не мне, я пропустил этот комментарий. Хабр теперь иногда не все новые комментарии показывает.
Это все равно не дает ответ на мой вопрос. Вы не описали, как именно они решают проблему, а просто сказали что решают. С моей точки зрения они ничего не решают, от упавшего канала проблемы те же, что и с REST.

Кодировать явно меньше, чем в случае REST, благодаря средствам "из коробки", позволяющим укладывать входящие сообщения прямо в БД

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

bootstrap сервер отвечает только за выбор брокера для клиента
Так как трафик с самим bootstrap ничтожен

О, ну вот мы наконец и дошли до главного. То есть коннектиться в один и тот же ЦОД можно, если трафик ничтожен. В большинстве случаев то что вы называете "по одному каналу в Москву" считается ничтожным трафиком, так как рассчитано на нормальный канал. Там стоит программа-балансировщик, которая отвечает за выбор сервера, который будет обрабатывать запрос. Их может быть 100 штук в одном цод, потому что пользователей много. То есть всё то же самое, разница только в критериях оценки трафика.

У очередей конечно есть свои плюсы, но также и минусы, например объем ресурсов сервера для поддержания коннектов.

при условии, что данные по предыдущему запросу уже откоммичены клиентом

Ну так я про это условие и говорю. С REST можно сделать точно так же. Запрос на создание идет на случайный цод, запрос на получение состояния только на ЦОД создания.

Тут даже не нужно передавать в идентификаторе номер цода и обрабатывать его в балансировщике. Запрос на создание идет на api.gis-gkh.com, в ответе приходит прямая ссылка для отслеживания habarovsk-api.gis-gkh.com/query/123. Всё в полном соответствии с REST и stateless, и без лишнего трафика.

Но в ГИС ЖКХ видимо решили сделать более централизовано, только не учли падение канала. Итого, в REST можно сделать нормально, ГИС ЖКХ так не сделали, REST ни при чем, обсуждение можно завершать)

обсуждение можно завершать

Вот только мы ушли от изначально темы в подробности реализации, так и не придя к конценсусу относительно того, вписывается ли сохранение статуса клиента в парадигму stateless. Как минимум в разрезе статуса "забрал" клиент ранее подготовленные для него временные данные, или "не забрал". Коммитов в REST нет.

Просто "укладывать сообщения в БД" в сложных системах обычно никому не нужно, на то они и сложные.

Аж интересно стало, а в чем по-вашему проблема? На моих проектах такое массово используется.

вписывается ли сохранение статуса клиента в парадигму stateless

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

Условно говоря, если вы получили ссылку habarovsk-api.gis-gkh.com/query/123, открыли режим инкогнито в браузере, открыли эту ссылку или запустили fetch в консоли с нужными заголовками авторизации, и получили информацию о query 123, значит результат этого запроса не привязан к сессии, значит это stateless.

Аж интересно стало, а в чем по-вашему проблема?

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

Например, есть система для поставщиков интернет-магазина. Они добавляют изображения и описание товара и отправляют товар на модерацию менеджерам. Бизнес хочет, чтобы поставщики не загружали маленькие изображения. Поэтому API не может просто сохранять все изображения, оно должно проверить размеры и вернуть ошибку, если они меньше 600px. Если проверка прошла, API сохраняет файл в файловое хранилище (например Amazon S3), а запись о нем в базу.

В запросе "Отправить на модерацию" приходит только id товара, тут даже нечего ложить в базу. API должно достать товар из базы и проверить, что в товаре заполнено описание не меньше 300 символов, и есть не меньше одного изображения. Если не соответствует, вернуть понятные сообщения, что поставщик должен исправить. Если соответствует, поставить в базе статус "На модерации" и отправить уведомление в другую систему. Когда товар на модерации, API не должно разрешать загружать изображения и менять описание.

Если оно представлено как обычный ресурс и не зависит от сессии, то вписывается.

Уточните, пожалуйста. Именно в том разрезе, что я описал: "в разрезе статуса "забрал" клиент ранее подготовленные для него временные данные, или "не забрал". Коммитов в REST нет."

если вы получили ссылку habarovsk-api.gis-gkh.com/query/123, открыли режим инкогнито в браузере, открыли эту ссылку или запустили fetch в консоли с нужными заголовками авторизации, и получили информацию о query 123, значит результат этого запроса не привязан к сессии, значит это stateless.

Но это же не так. Данные асинхронного запроса хранятся временно. В разное время получите разный результат.

оно должно проверить размеры и вернуть ошибку, если они меньше 600px.

И кто мешает проверить это в БД триггером и вернуть ошибку? Сообщение автоматически улетит в dlq с соответствующим хедером, где с ним можно будет разобраться уже на стороне клиента.

приходит только id товара, тут даже нечего ложить в базу

В базу стоит писать все для сохранения аудиторского следа.

поставить в базе статус "На модерации" и отправить уведомление в другую систему

Писал уже выше. Не поставить статус, а создать связанный документ. Это на фронте свои статусы изображайте, а не в БД. А новый документ уже автоматически подобрать, например, Debezium.

должно достать товар из базы и проверить

Аналогично - триггером в БД. В чем проблемы?

И кто мешает проверить это в БД триггером и вернуть ошибку?

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

Да ничего не мешает!

Кроме полной непригодности встроенных средств СУБД к решению данной задачи

Вы из криокамеры?

застрявших в прошлом веке языков программирования триггеров

Что Вы имеете против Python, Java, Rust?

Уточните, пожалуйста.

Из того, что вы написали далее, непонятно, что нужно уточнять. В моем представлении мое утверждение и есть в том разрезе, который вы указали.

Данные асинхронного запроса хранятся временно

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

В базу стоит писать все для сохранения аудиторского следа.

id товара уже и так записан в базе.
Логи что запрос пришел это другой вопрос, подразумевается, что они есть в нужном объеме.

Писал уже выше. Не поставить статус, а создать связанный документ.

А я вам выше ответил. Бизнес хочет статус, а не связанный документ.

Это на фронте свои статусы изображайте, а не в БД.

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

А новый документ уже автоматически подобрать, например, Debezium.

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

И кто мешает проверить это в БД триггером и вернуть ошибку?

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

Сможете скинуть ссылки на библиотеки для БД, которые это делают, и написать код для их вызова в комментарии? Я в этом сомневаюсь. А я для PHP могу.

Аналогично - триггером в БД. В чем проблемы?

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

Причем вы похоже не понимаете, что логически это то же самое. Приходят данные, вызывается логика на каком-то языке, которая разрешает или запрещает сохранять данные. Непонятно, почему это должна делать именно база, а не приложение. К тому же приложение я могу запустить хоть 100 инстансов на 10 серверах для балансировки нагрузки на процессор, а с базой так не получится.

В моем представлении мое утверждение и есть в том разрезе, который вы указали.

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

И речь идет именно об этом.

пользователь написал комментарий на каком-то сайте, а потом его удалил. Или загрузил изображение в товар, а потом его удалил. Это такие же временные данные

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

Бизнес хочет статус, а не связанный документ.

А я и ответил: "на фронте свои статусы изображайте, а не в БД"

что хочу, то и делаю

Это Вы называете профессиональным подходом? UPDATE индексированных полей - прямая дорога к dead lock.

Вы мне предлагаете вместо одного UPDATE

Я не то что предлагаю отказаться от UPDATE индексированных полей. Я не пропущу такой PR. Мало того, что это может привести к утере аудиторского следа, это еще гарантированные ловли dead locks и последующее костылестроение через advisory lock.

Как проверить размер JPEG средствами БД

То, что Вы не представляете как это сделать на Python или Java говорит исключительно о незнании Вами этих языков. Не более того.

подключиться к AWS, я даже представить не могу.

А вот блокирующие операции из СУБД делать не стоит. Пишите в таблицу-очередь в БД, а оттуда уже, хоть тем же Debezium, отправляйте в обработку на внешние сервисы.

Сможете скинуть ссылки на библиотеки для БД, которые это делают, и написать код для их вызова в комментарии?

А Вы сами не можете погуглить их для Python, Java или Rust?

для PHP могу

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

Непонятно, почему это должна делать именно база, а не приложение.

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

Hidden text

И речь идет именно об этом.

Я понял, что речь идет об этом, я не понял, что именно я должен уточнить.

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

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

это может привести к утере аудиторского следа

Еще раз повторяю - в моем примере бизнесу не нужен аудиторский след для этих данных.

А я и ответил: "на фронте свои статусы изображайте, а не в БД"

И что с того, что вы ответили? Это как-то отменяет факт, что бизнес хочет статус, а не связанный документ?
Я не понимаю, почему вы считаете себя владельцем нашей БД и указываете нам, что там хранить. Статусы наши, и БД тоже наша. Мы хотим хранить там статус, ваше мнение нас не интересует.

Это Вы называете профессиональным подходом?

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

UPDATE индексированных полей - прямая дорога к dead lock.
это еще гарантированные ловли dead locks

В моей практике никогда не было dead lock из-за UPDATE поля status или любых других полей конкретной записи, в том числе индексированных. Если они у вас возникают, возможно вы что-то делаете не так. Возможно это связано с использованием триггеров. Вероятность этого вы точно оцениваете неправильно.

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

То, что Вы не представляете как это сделать на Python или Java
А Вы сами не можете погуглить их для Python, Java или Rust?

В моей БД нет никакого Python, Java или Rust. Это достаточный ответ на ваш вопрос "что мешает проверить в БД триггером?"?

Как это сделать на Python или Java, я представляю. Я не представляю, почему надо превращать БД в веб-сервер, который при запросе от пользователя вызывает код на этих языках в виде триггеров БД. Условный nginx с этим справляется гораздо лучше.

Пишите в таблицу-очередь в БД

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

А вот наличие уже готовых компонентов
действительно делает разработку внутри локальной СУБД микросервиса дешевле, быстрее и надежней

Готовые компоненты на Java прекрасно вызываются и из приложения на Java, а не из БД.
То есть у нас есть готовый компонент на Java, который проверяет размеры JPEG. Вы предлагаете вызывать этот код из БД. Я предлагаю вызывать этот код из приложения на Java. Почему вдруг разработка внутри базы становится быстрее, если это один и тот же код?

В моей практике никогда не было dead lock из-за UPDATE поля status или любых других полей конкретной записи, в том числе индексированных.

Из этого следует лишь, что или у Вас весьма незначительна конкурентная нагрузка на БД, или неэффективное использование БД, где в одной транзакции обновляется индексированное поле не более, чем в одной строке.

при множественном UPDATE в разном порядке в разных процессах

Вот только порядком блокирования страниц индексов Вы не управляете. Отсюда и высокие шансы dead lock.

пользователю вернется сообщение

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

В моей БД нет никакого Python, Java или Rust.

Переходите на PostgreSQL. Все равно в РФ сейчас ему альтернативы просто нет.

Условный nginx с этим справляется гораздо лучше.

С чего Вы это взяли? Так же форкается и так же вызывает код в уже загруженной и инициализированной so.

Я прекрасно и из приложения отправлю.

Не написав ни строчки кода? Очень сомневаюсь

Почему вдруг разработка внутри базы становится быстрее, если это один и тот же код?

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

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

Готовые компоненты на Java прекрасно вызываются и из приложения на Java,

Вот как раз на Java нет проблем в подключении имеющихся специально оформленных классов к Sink или Debezium только конфиграцией его или схемы. Но есть ли смысл регистрации класса в Confluent, если он используется только один раз в одном компоненте?

Hidden text

Из этого следует лишь, что или у Вас весьма незначительна конкурентная нагрузка на БД

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

Мы же, надеюсь, не о тупом веб-сайте говорим, а действительно о сложной системе?

Мы говорим о сложной логике в одном конкретном сервисе, неважно сколько там потоков и других сервисов.

Переходите на PostgreSQL

Ну да, теперь я еще и всю базу должен переписать. Нет, мне удобнее просто написать код в приложении без триггеров в БД.

С чего Вы это взяли?

Из практики.

Не написав ни строчки кода?

А это вы откуда взяли? Я такого не говорил.
Не написав таблицу-очередь и код для работы с ней.

Потому что код приходится писать только для таких редких случаев, как Ваш JPEG.

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

Сохранил документ, а минут через пять приходит ему уведомление о том, что возникла ошибка разноски по причине от него вообще не зависящей.

Нет. Это у вас ему придет через несколько минут, а у меня сообщение об ошибке возвращается сразу в ответе на веб-запрос, и документ вообще не сохраняется.

Ну и Вы почему то проигнорировали мое замечание
локальный сетевой сокет

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

это становится очень заметным

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

Далее не вижу смысла спорить о логике в БД без примеров кода.

Мы говорим о сложной логике в одном конкретном сервисе

Я уже указывал, что в безнадежно устаревшей монолитной архитектуре действительно БД является узким местом и грузить еще чем-то, что может выполняться не в ней - не стоит.

Из практики.

И откуда у Вас взялась практика использования функций и хранимых процедур на том же Rust, если в Вашей БД нет его поддержки? К тому же использование ngix в качестве сервера приложений - странная идея. Разве он поддерживает многопоточную конвееризацию?

Для любых входных данных нужна какая-то валидация

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

у меня сообщение об ошибке возвращается сразу в ответе на веб-запрос, и документ вообще не сохраняется.

Вот видите, у Вас мелкий проект, где такой подход допустим. А если я попробую обрабатывать сотни тысяч сообщений по одному, а не пакетно конвеерами между микросервисами, то мне понадобится суммарно не несколько сотен ядер в k8s, а несколько тысяч.

Вы можете себе позволить "сложную логику" одного сервиса, а я вынужден делать микросервисы максимально простыми и независимыми, потому что в противном случае поддерживать такое станет слишком трудоемко. Зато большинство этих микросервисов оказываются low-code.

Я проверял.

И это после

В моей БД нет никакого Python, Java или Rust.

Что-то не сходится.

где тут "обработка данных только в памяти".

Например, очень упрощенно, валидация пришедших 100 тыс. показаний ПУ выполняется с обязательной связью между предыдущими и последующими показаниями (данные могут прийти задним числом). И считать при валидации эти 100 тыс. уже имеющихся показаний в память СУБД намного эффективней, чем еще и передавать их клиенту через сокет. А в случае АСКУЭ такие массивы прилетают каждые 5-10-15 минут. А 100 тыс. - это только район или даже микрорайон многоэтажек. То есть дальше, уже на других микросервисах, пойдет еще и валидация агрегатов с показаниями технических ПУ на подстанциях.

просто запускаю нужное количество инстансов веб-сервера

А с таким подходом мне и 10 тыс. ядер не хватит в k8s

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

Я уже указывал, что в безнадежно устаревшей монолитной архитектуре

Я не знаю, зачем вы это указывали, я не говорил про монолитную архитектуру.

И откуда у Вас взялась практика использования функций и хранимых процедур на том же Rust, если в Вашей БД нет его поддержки?
Что-то не сходится.

Я не говорил, что у меня есть практика использования хранимых процедур на Rust. Хранимые процедуры на PL/SQL я использовал. Синтаксис это лишь одна из проблем триггеров, остальные от синтаксиса не зависят.

и провалидировать их консистентность можно только в конечном итоге и далеко не сразу

Ну у вас может и нельзя, а у нас можно. И бизнес требует не сохранять в базу неправильные данные. Если нужны логи, они пишутся отдельно от бизнес-сущностей.

А не сохранять сырые данные в БД, обычно, не допустимо.

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

Вот видите, у Вас мелкий проект, где такой подход допустим.

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

а я вынужден делать микросервисы максимально простыми и независимыми
Зато большинство этих микросервисов оказываются low-code.

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

Если сложить весь low-code, то получится такой же объем кода, что и без микросервисов.

считать при валидации эти 100 тыс. уже имеющихся показаний в память СУБД намного эффективней

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

Проверить 100 тысяч чисел раз в 10 минут? Это довольно мало.
Для сравнения, у нас были CSV-файлы с товарами поставщиков на 100 тысяч строк и 30 колонок, каждую колонку надо провалидировать, сохранить данные в базу, скачать изображения по ссылкам и загрузить их в AWS. Один файл обрабатывался часа 2 (естественно в фоновом процессе, а не в веб-запросе), это всего лишь в 12 раз больше, чем 10 минут, но и обработка тут не просто сравнить 2 числа. Использовалось кажется 3 инстанса веб-сервера и 1 база на всё, включая работу пользователей.

Вопрос как отправлять в брокер из браузера вы проигнорировали.

Я просто не понимаю, что Вы имеете ввиду. Руками из браузера клавиатурой и мышью? - Например, через UI for Apache Kafka, но есть масса и других готовых веб-морд для этого. Можно и REST запросом через родной Confluent REST Proxy, но не все браузеры позволяют отправлять POST запросы. В продуктивной среде это возлагается на веб-приложение со своей мордой по требованиям заказчика, задача которого только поместить полученные данные в свою БД, откуда их уже автоматически подберет Debezium.

Если бизнес требует возвращать ошибки валидации сразу, значит вы должны возвращать их сразу.

А он по любому "сразу" ничего не получит. На все нужно время. Например: https://developer.confluent.io/learn/kafka-performance/
Кто из Ваших пользователей эти 5 мс заметит?

К тому же у Вас очень простая валидация. Даже примитивная накладная ЭТРАН валидируется минуты. Так как должна учесть загруженность станции, подъездных путей, состояния вагонов, доступность маневрового локомотива и т.п. И только после всех проверок и расчетов она из заготовки либо превратится в накладную, либо окажется отмененной.

Вы говорите про вертикальное масшатбирование, а я про горизонтальное.

Вы невнимательно читали: "а 100 тыс. - это только район или даже микрорайон многоэтажек.". Даже небольшой ТГК-2 - это несколько миллионов показаний ПУ каждые 5-10-15 минут. Про ТГК-1 вообще молчу. Каждый микросервис обслуживает только свой район (точнее, свою подстанцию). Вот такая вынужденная мера, чтобы не хранить в локальной БД каждого микросервиса миллиарды показаний вообще всех ПУ, необходимых для валидации.

Проверить 100 тысяч чисел раз в 10 минут? Это довольно мало.

Успехов. Напишите валидацию показания ПУ на основании уже имеющихся нескольких сотен показаний по этому ПУ уже имеющихся в БД. То есть считайте из БД по сокету 15 миллионов показаний, для того чтобы валидировать 100 тысяч пришедших. И не забудьте хотя бы медиану по потреблению каждой фазы пересчитывать. Потому что некоторые формулы валидации опираются именно на медианное потребление за последние 24 часа.

у нас были CSV-файлы с товарами поставщиков на 100 тысяч строк и 30 колонок

Вы прикалываетесь? Сколько по-вашему регистров в среднестатистическом ПУ? А какой-нибудь бытовой трехфазный Пульсар легко может выдать, кроме значений 40 регистров, еще и список из 37 типов событий, каждое глубиной до 24 временных меток - в сумме список пар тип события - время события может почти до 1000 доходить. И все эти значения влияют на валидацию.

В продуктивной среде это возлагается на веб-приложение со своей мордой

Бинго! То есть вам тоже не удалось избавиться от веб-приложения со своим бэкендом (судя по наличию у него отдельной "морды", должна быть и отдельная задняя часть).

А ваши прошлые сообщения выглядели так, как будто одна только база способна всё разрулить.

Даже примитивная накладная ЭТРАН валидируется минуты

И эти люди учат нас работать с БД...

Каждый микросервис обслуживает только свой район (точнее, свою подстанцию).

Пожалуйста, используйте нормальную терминологию, тогда ваши комментарии будет проще понимать! Если микросервисы делают одно и то же, но над разными данными - это не разные микросервисы, а разные экземпляры одного микросервиса.

То есть считайте из БД по сокету 15 миллионов показаний, для того чтобы валидировать 100 тысяч пришедших.

Считывание 15 миллионов показаний по сокету, конечно же, сильно сложнее считывания 15 миллионов показаний с диска - но и то и другое хрень.

Вот не поверю что вы и правда так поступаете не используя никакой агрегации.

не удалось избавиться от веб-приложения со своим бэкендом

А никто и не предлагалал от него вообще избавиться. Пользовательский интерфейс нужен по любому. Даже если он составляет 3-5% от кода всей системы.

И эти люди учат нас работать с БД...

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

но и то и другое хрень

Покажите тогда код. Я проверю, как он за несколько минут хотя бы 100 тыс. медиан насчитает.

не используя никакой агрегации

Вы знаете как агрегировать медиану?

Ну научите, как можно собрать информации из доброго десятка удаленных БД быстрее.

Параллельно, заранее и двухэтапно.

Проверка загруженности станции - ну вот вообще не задача валидации. Валидация должна проверить что станция существует, а загруженность будет проверяться на этапе согласования.

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

Классические распределённые транзакции с десятком удалённых баз по EDGE? Ну и ну.

Я просто не понимаю, что Вы имеете ввиду. Руками из браузера клавиатурой и мышью?

Я же написал пример. Есть приложение, в котором работают пользователи-поставщики. Интерфейс это фронтенд-приложение на JavaScript, которое работает в браузере.
Сейчас оно отправляет веб-запросы на веб-сервер с nginx, который запускает код API, который обрабатывает данные из запроса. Вы предлагаете вместо nginx использовать брокеры с базой.

В продуктивной среде это возлагается на веб-приложение со своей мордой
задача которого только поместить полученные данные в свою БД

Ну то есть без nginx и кода перед базой, который сохранит данные в базу, никак не обойтись, что и требовалось доказать.

Кто из Ваших пользователей эти 5 мс заметит?

А откуда взялось 5 мс? Отставание очереди может и часы составлять.
В моем варианте пользователь гарантировано получит успешный запрос или ошибку в течение нескольких секунд.

К тому же у Вас очень простая валидация.

Еще раз задаю вопрос, и? Ее от этого не надо делать, или что?
Мне надо, я делаю, триггеры с очередями для нее не подходят.

Вот такая вынужденная мера, чтобы не хранить в локальной БД каждого микросервиса миллиарды показаний

Я не знаю, откуда вы берете такие идеи, я ничего подобного не предлагал.
Шардинг это нормальное средство, но когда когда база не занимается посторонними расчетами, то и необходимость в нем возникает реже.

Сколько по-вашему регистров в среднестатистическом ПУ?

Не знаю, вы сказали 100 тысяч показаний, одно показание обычно означает одно число.

То есть считайте из БД по сокету 15 миллионов показаний, для того чтобы валидировать 100 тысяч пришедших.

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

Есть приложение, в котором работают пользователи-поставщики. Интерфейс это фронтенд-приложение на JavaScript, которое работает в браузере. Сейчас оно отправляет веб-запросы на веб-сервер с nginx, который запускает код API, который обрабатывает данные из запроса. Вы предлагаете вместо nginx использовать брокеры с базой.

"Смешались в кучу кони, люди" (с). Так как поставщики условного X5 могут лепить миллионы операций ежедневно, я предлагаю с бека снять нагрузку валидации, разноски и прочей workflow этих операций, оставив лишь нагрузку сохранения этих операций в своей локальной БД и вынимания из БД ответов сервисов, обеспечивающих это самое workflow. По той простой причине, что тогда не надо будет иметь петабайтные БД на каждом экземпляре веб-сервера и постоянно пасти их мульти-мастер логическую репликацию.

А откуда взялось 5 мс?

По ссылке. 99% сообщений при средней нагрузке на партицию в 200 МБ/сек обработались за 5 мс. Остальной 1% - за 16 мс.

Не знаю, вы сказали 100 тысяч показаний, одно показание обычно означает одно число.

Даже без АСКУЭ передают показания хотя бы двумя числами по ночному и дневному тарифу. А сколько регистров в ПУ можно посмотреть даже просто потыкав на кнопки. Он много расскажет и о напряжении, и токе, и реактивке, и частоте, и фазовых искажениях.

Я сомневаюсь, что Гугл работает на триггерах в БД

Сомневайтесь. А я знаю что даже Hana обвешена триггерами, хранимыми процедурами и коннекторами, как елка под снегом. А все потому, что SAP строго транзакционная система, а не совершенно не транзакционный поисковик, как Гугл.

По той простой причине, что тогда не надо будет иметь петабайтные БД на каждом экземпляре веб-сервера

Еще раз говорю, это ваши фантазии, я не предлагаю ничего подобного. Вы видимо даже ответы не читаете.

В моем варианте большая база одна, пока она справляется с вводом-выводом. Каждый экземпляр веб-сервера подключается к ней. Нетранзакционные SELECT-ы базе делать проще, чем транзакционные триггеры на INSERT с несколькими SELECT-ами внутри. Когда база не справляется, делается реплика или шардинг. Число веб-серверов к числу баз не привязано.

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

а не совершенно не транзакционный поисковик, как Гугл

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

Кстати, я вообще не понимаю, сначала вы заявили "Кодировать явно меньше", спросили зачем мне логика обработки, а теперь советуете писать код. Ну так я и говорил вам о том, что для API в большинстве случаев надо писать код.

Публикации