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

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

Ошибки должны быть информативными

Я выработал для себя правило (но не изобрел его, само собой), что сообщение об ошибке должно строиться по шаблону "что случилось - почему так случилось - что с этим делать". Что-то вроде "Невозможно выполнить загрузку. Удаленный сервер недоступен. Попробуйте выполнить загрузку позднее или обратитесь в администратору. Детали ошибки: ххх". Просто ужасно, когда люди не парятся и просто швыряют в клиента exception.getMessage() не глядя.

Это весьма дельная мысль, спасибо.

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

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

Хорошо: POST /orders/cancel

Отменяет который заказ? Последний? Все? Почему хорошо удалять заказ неидемпотентным методом создания ресурса?

Хорошо: POST /v1/orders/statistics/aggregate

И что хорошего в запросе данных некешируемым методом создания ресурса?

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

По POST /v1/orders/statistics/aggregate как-то не очень очевидно, что оно не модифицирующее.

Плохо: "date": "11/12/2020" — существует огромное количество стандартов записи дат,

Международный стандарт только один - ISO8601.

Хорошо: "duration_ms": 5000, "duration": "5000ms", ...

'PT5S'. Читайте IS8601 и не изобретайте своих "стандартов".

Хорошо: begin_transition / end_transition

Так этот "конец транзакции" применяет её или отменяет?

Ошибка: доставка становится бесплатной при стоимости заказа от 9000 тугриков

А если стоимость заказа меньше 9000, то в качестве delivery_fee и total клиент сможет указывать любое положительное число? Скажите же скорее адрес этого магазина - скуплю его за бесценок.

Возвращает указанный limit записей, отсортированных по дате создания, начиная с первой записи, созданной позднее, чем запись с указанным id

Этот метод нормально работает только с сортировкой по индексу без фильтрации.

Другой вариант организации таких списков — возврат курсора cursor, который используется вместо record_id

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

подход с курсором не означает, что limit/offset использовать нельзя — ничто не мешает сделать двойной интерфейс

И время запроса будет пропорционально величине offset.

// Подтверждает черновик заказа PUT /v1/orders/drafts/{draft_id}

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

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

Два пользователя изменили в оффлайне одно и то же поле одного и того же объекта (поправили описание товара, например). Как такой формат патчей поможет "перебазировать" изменения не потеряв изменения?

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

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

Отменяет который заказ? Последний? Все?

Тот, который указан в теле запроса

Почему хорошо удалять заказ неидемпотентным методом создания ресурса?

POST не является методом создания ресурса. «The POST method requests that the target resource process the representation enclosed in the request according to the resource's own specific semantics» — https://www.rfc-editor.org/rfc/rfc7231#page-25

То, что метод может быть неидемпотентным не означает, что он обязан быть идемпотентным (как раз наоборот, далее в тексте предлагается все методы обязательно делать идемпотентными). POST здесь используется именно для того, чтобы соответствовать процитированной семантике HTTP-методов согласно RFC. `PUT /orders/{id}/cancellation` тоже допустим.

И что хорошего в запросе данных некешируемым методом создания ресурса?

POST не является методом создания ресурса. «The POST method requests
that the target resource process the representation enclosed in the
request according to the resource's own specific semantics» — https://www.rfc-editor.org/rfc/rfc7231#page-25

В вопросе кэширования результатов «тяжёлых» вычислений нас интересует прежде всего серверный кэш, а не клиентский, правда же? Доступ к нему вполне можно и через POST организовать — вновь вопрос того, что мы считаем важным: индицировать семантику операции (вы запускаете сложный алгоритм) или сэкономить какие-то байты на клиентском кэшировании.

Наконец, результаты POST могут кэшироваться (хотя я лично не рекомендую это делать), см. тот же RFC.

Нет.

Международный стандарт только один - ISO8601

Во-первых, это неправда. Есть ещё как минимум RFC 3339 https://www.rfc-editor.org/rfc/rfc3339 и RFC 7231 https://www.rfc-editor.org/rfc/rfc7231#section-7.1.1.1

Во-вторых, независимо от отсутствия / наличия единого стандарта, даты в интернете вы можете получить в каком угодно виде. См. вторую часть фразы — «Исключения возможны только там, где вы на 100% уверены, что в мире существует только один стандарт для этой сущности, и всё население земного шара о нём в курсе

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

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

Первый - подмножество ISO8601

Нет.

В частности, 2022-09-19 16:46:00 — валидная дата с т.з. RFC 3339, но невалидная с т.з. ISO8601. Запись 2022-09-19T16:46:00-00:00 валидна с т.з RFC, но невалидна с т.з. ISO

Так этот "конец транзакции" применяет её или отменяет?

В тексте не transaction, а transition. Если существует двусмысленность (любого из терминов), следует выбрать не-двусмысленный вариант (для обоих терминов).

Да без разницы, transition тоже можно как завершить, так и отменить.

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

Получится - он вернётся в исходное положение, а не финальное.

А если стоимость заказа меньше 9000, то в качестве delivery_fee и total
клиент сможет указывать любое положительное число? Скажите же скорее
адрес этого магазина - скуплю его за бесценок.

Я не понял этого комментария.

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

Там, где речь идёт о деньгах, обычно лучше передать с клиента ту цифру, которую клиент видел глазами (напрямую в виде значения, или закодированную в offer_id) и валидировать её на сервере. Потому что между этими двумя моментами (клиент видел цифру стоимости доставки — клиент подтвердил заказ) что-то могло измениться (закончилась скидка, увеличился сурж), и клиент будет неприятно удивлён.

Всё это и так фиксируется в оффере, по которому вы собираетесь валидировать.

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

Этот метод нормально работает только с сортировкой по индексу без фильтрации.

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

И время запроса будет пропорционально величине offset.

Я не обсуждаю здесь детали технической имплементации.

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

Действительно, URL должен быть `PUT /v1/orders/drafts/{draft_id}/confirm`. Поправлю, спасибо.

Два пользователя изменили в оффлайне одно и то же поле одного и того же
объекта (поправили описание товара, например). Как такой формат патчей
поможет "перебазировать" изменения не потеряв изменения?

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

Ну значит CRDT творят чудеса.

CRDT и представляют формат описания атомарных изменений, в точности как в тексте описано.

Даже близко не похоже.

Два пользователя изменили в оффлайне одно и то же поле одного и того же объекта

Для разрешения ситуации с потерянным обновлением надо применять родные средства http – условные запросы. Ответ на запрос состояния объекта перед редактированием возвращает вам Etag. А в запрос редактирования вы передается условие If-Match.

Автор в статье указал пример с публикацией нового ресурса через POST. По спецификации метод POST не обязан быть идемпотентным. Но вполне логично предположить, что вряд ли при случайном повторном запросе (от браузера / api-gateway или какого промежуточного прокси) вы захотите, чтобы создавался дубль объекта. Для обхода этой ситуации автор предлагает на клиенте генерировать некий ИД, по которому на сервере мы будем определять - приходил ли уже такой запрос или нет. Если приходил, то повторно ничего создавать не надо.

А в запрос редактирования вы передается условие If-Match.

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

Предлагаете другому потерять исправление опечатки?

Это уже вопрос совместного доступа и резолва конфликтов. Это проблема уровня приложения, а не транспорта. Транспорт вам тут никак не поможет.

Предлагаю ничего не терять. CRDT разрешает конфликты на уровне транспорта.

CRDT работает для примитивов (чисел), и даже для них необходимо явное указание merge функции (например, max).

В случае текстов это не работает. Простой логический эксперимент, двое людей загрузили состояние объекта С0 и сделали правки: С1 и С2.. как их смержить между собой? В общем общем случае - никак. Можно лишь найти ухищрения, сузить области изменений. Например, если первый правил в одном абзаце, а второй во втором, то все легко мержится. Или если сузить область до уровня строк (как и делает git merge), и тогда построчно смотреть, что добавилось, а что изменилось.

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

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

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

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

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

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

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

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

GIT появился в 2005. Самое раннее упоминание CRDT - 2006.

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

Приведу пример, опираясь на всю тот же кейс заказа кофе. Допустим, пользователь создал заказ на 200 мл капучино, а потом с двух разных устройств отредактировал его — на одном выставил 400 мл, на другом 500 мл.

Логика CRBT диктует нам, что для транзитивности этих изменений нужно представить их не как абсолюты, а как транзитивные изменения — +200 и +300 мл соответственно. И результат мержа двух операций тогда — 700 мл капучино. Но как бы здравый смысл подсказывает нам, что вот чего пользователь не хотел абсолютно точно, так это 700 мл капучино.

Мы даже можем дать ему интерфейс, в котором он не может выбрать цифру, а только жать кнопки +100 / -100, и формально мы тогда сможем применить CRBT-формат. Но в реальном мире ситуация не изменилась: пользователь абсолютно точно не хотел сделать «+100» — он хотел выставить объём, а наши транзитивные операции генерировал просто потому, что другой возможности ему не предоставили.

CRDT ничего подобного не диктует. Тут следует использовать LWW-Register, а не описанный вами PN-Counter.

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

ХЗ, что за "обычный PATCH", но я вам всё же рекомендую самостоятельно почитать про CRDT, а не заниматься демагогией.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории