company_banner

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

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


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


    image


    Про API


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


    {
      "from": "Москва, ул. Садовническая набережная 82с2",
      "to": "Аэропорт Внуково"
    }

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


    {
      "orders": [
        {
          "id": 1,
          "from": "Москва, ул. Садовническая набережная 82с2",
          "to": "Аэропорт Внуково"
        }
      ]
    }

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


    1. при старте приложения вызываем GET /v1/orders, если получили активный заказ, то рисуем в UI его состояние;
    2. при нажатии на кнопку «заказать такси» вызываем POST /v1/orders с введенными пользовательскими данными;
    3. при возникновении любой ошибки сервера или сетевой ошибки рисуем сообщение об ошибке и больше ничего не делаем.

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


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


    image


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


    image


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


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


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


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


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


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


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


    1. начать транзакцию;
    2. UPDATE active_orders SET n=1 WHERE user_id={user_id} AND n=0;

    3. если update изменил 0 записей, то отдать HTTP код 409;
    4. вставить объект заказа в другую таблицу;
    5. завершить транзакцию.

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


    Мультизаказ


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


    image


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


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


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


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


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


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


    {
      "from": "Москва, ул. Садовническая набережная 82с2",
      "to": "Аэропорт Внуково",
      "idempotency_key": "786706b8-ed80-443a-80f6-ea1fa8cc1b51"
    }

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


    image


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


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


    1. пользователь хочет создать заказ, запрос приходит на сервер, заказ создается, тестировщики эмулируют сетевую ошибку и ответ приложение не получает;
    2. пользователь видит сообщение об ошибке, по какой-то причине перед этим еще меняет точку назначения, и только после нажимает на кнопку создания такси еще раз;
    3. приложение не меняет ключ идемпотентности между запросами;
    4. сервер обнаруживает, что заказ с таким ключом идемпотентности уже есть и отдает 200;
    5. на сервере создан заказ со старой точкой назначения, а пользователь думает что он создан с новой точкой назначения, и уезжает не туда.

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


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


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


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


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


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


    1. приложение отправляет запрос на создание заказа, запрос выполняется десятки секунд по каким-то причинам, заказ медленно создается;
    2. пользователь не может ничего сделать в приложении, при этом и такси не заказывается, тогда он решает полностью выгрузить приложение из памяти;
    3. пользователь заново открывает приложение, оно делает запрос GET /v1/orders, и создающегося в данный момент заказа не получает, так как он еще не создался до конца;
    4. пользователь думает, что приложение сглючило и делает заказ еще раз, на этот раз заказ создается быстро;
    5. создание первого заказа отвисло, и заказ создался до конца;
    6. к пассажиру приезжает два такси.

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


    1. приложение отправляет запрос на создание заказа, заказ создается, но мобильная сеть лагает, и приложение не получает ответ об успешном создании заказа;
    2. диспетчер, либо сам пользователь через пуш по какой-то причине отменяет заказ: отмена заказа сделана как удаление строки из таблицы базы данных;
    3. приложение шлет повторный запрос на создание заказа: запрос успешно выполняется и создается еще один заказ, так как ключ идемпотентности, хранившийся в прошлом заказе, больше не существует в таблице.

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


    1. сценарий 1: приложение хранит у себя все создающиеся в данный момент заказы даже между рестартами приложения. Приложение показывает их в интерфейсе сразу после старта, продолжая попытки их создания, при условии что прошло не слишком много времени с момента их создания.
    2. сценарий 2: перейти от удаления записей из таблицы заказов к выставлению поля deleted_at=now() — так называемому soft delete. Тогда ограничение уникальности ключа идемпотентности работало бы и для отмененных заказов.
    3. сценарий 3: отделить абстракцию обеспечения идемпотентности запросов от абстракции ресурсов и хранить использованные ключи идемпотентности ограниченное время отдельно от ресурса, например, 24ч.

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


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


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


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


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


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


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


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


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


    1. из базы данных он стал селектить все, даже уже отмененные заказы с данным id;
    2. если заказ уже был удален, и это было в пределах последних n минут (то есть, на обычных перезапросах), то сервер стал отдавать 200;
    3. в остальных случаях сервер отдает 410 с ошибкой «заказа не существует». Вася решил попутно заменить 404 на 410 как более подходящий, так как код 404 означает, что это ошибка временная, а запрос можно потом повторить. Код 410 же означает, что ошибка постоянная, и повтор запроса выдаст тот же результат.

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


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


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


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


    image


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


    {
      "to": "новая точка назначения"
    }

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


    UPDATE orders SET to={to} WHERE id={id}

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


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


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


    UPDATE user_counters SET orders_finished = {orders_finished+1} WHERE user_id={user_id}

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


    image


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


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


    1. при создании заказа счетчик никак не меняется;
    2. в очереди заданий появляется новая процедура, которая фетчит все заказы пользователя из обоих хранилищ, рассчитывает метрику завершенных заказов и сохраняет ее в базу;
    3. задание в очередь кладется из API завершения заказа: при повторных вызовах API в худшем случае несколько раз выполнится задание в очереди, что нестрашно.

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


    Все проверил


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


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


    Дубли SMS


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


    image


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


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


    Заключение


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


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


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

    Яндекс
    1 301,78
    Как мы делаем Яндекс
    Поделиться публикацией

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

      –9
      Я все ждал что в конце концов Вася перейдет с Postgresql на MySQL…
      • НЛО прилетело и опубликовало эту надпись здесь
          +19
          Возможно отсылка к метаниям Uber с mysql и postgresql =)
            0
            А что там было?
            Я как-то на докладе слышал историю от mail.ru про их переход с mysql на postgesql. В числе прочего там были забавные баги mysql (вроде повтора значений, в некоторых условиях, автоинкрементного поля)
              +3

              Перевод статьи про миграцию Uber с Postgres на MySQL

                +3
                Там ребята давным давно сидели на Postgres, потом им захотелось в микросервисы и MySql. А потом подумалось — почему б с нова не пересесть с иглы MySQL на православный Postgres, но наверченная архитектура и специфичные вещи MySQL в дополнение к особенностям Postgesql с репликацией (и не только) заставили отказаться от этого решения.
                Вольный краткий пересказ, на истинность не претендую =)
            +4
            Или откажется от REST в пользу RPC (SOAP).
              +5
              Поясните, как бы это решило проблемы, описанные в статье?
                +4
                Примерно также, как переход с постгри на мускуль. Правда попутно ещё бы добавило боли в разработке как сервера, так и клиента (но это не точно).
                  –2

                  Извиняюсь за оффтоп, но


                  постгри

                  так лучше не писать и не говорить


                  PostgreSQL создана на основе некоммерческой СУБД Postgres… более раннего проекта Ingres… Название расшифровывалось как «Post Ingres»

                  https://ru.wikipedia.org/wiki/PostgreSQL

                    –5
                    Добавлю оффтопа тогда, MySQL некоторые называют майсиквел, вот так, как мне кажется, точно лучше не говорить (да и мускуль тоже не очень то хорошо, если уж на то пошло).
                    Да я просто привык уже говорить мускуль и постгря, это большинству разрабов сталкивавшихся с СУБД понятно. Многие разрабы, с которыми мне доводилось работать, говорили либо постгря, либо постгрес, лишь единицы называли полностью постгреэскуэл(ь), и ни разу не слышал «Пост-Грэс-Кью-Эл»
                      –3

                      Википедия вполне признаёт /ˈsiːkwəl/ "sequel":


                      SQL was initially developed at IBM by Donald D. Chamberlin and Raymond F. Boyce after learning about the relational model from Ted Codd in the early 1970s. This version, initially called SEQUEL (Structured English Query Language)...
                        –5
                        мускуль и постгря, это большинству разрабов...

                        Вы добавили непонятное слово «разраб».
                          +7

                          Что тут не понятного, раб на один раз.

                        –7
                        Россию тоже не принято называть Рашей, однако, кому это мешает?

                        image

                        Везде есть свой сленг. И ИМХО по русски более правильно будет сформированное слово «постгри» чем «постгрес». Но это тема другого сайта.
                +2
                Вопрос по архитектуре: Почему не использовать в данном случае локальную базу и синхронизацию с сервером? Есть какие-то подводные камни?
                  +3

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

                    +1
                    Ну и пользователь у которого заказ локально изменился и показывает это до синка, может со спокойной совестью выключить или потерять связь. А через пол часа соединившись с сервером, обнаружить, что сервер об этом так и не узнал.
                  +1
                  Васю то хоть не уволили в итоге?
                    +7

                    Из статьи стало понятно, что Васе нужен опыт

                      +6
                      Согласен, но для стажера он мыслит весьма не плохо) Раз умеет решать проблемы по мере поступления… У нас стажеры часто вступают в ступор…
                        +11
                        Что-то мне подсказывает, что за Васей скрыт реальный опыт разработчиков API такси…
                          +22
                          Кто ни разу не ронял прод — пусть первый бросит в меня камень…
                            +3

                            Опыт около-реальный и не только по работе в Яндекс.Такси. Но в Яндекс.Такси до продакшена при мне ничего из подобного не доходило, все отлавливалось на этапе дизайн-ревью.

                        +1
                        Насколько я помню, ваши коллеги из Яндекс.Кассы проблему решили на высоком уровне kassa.yandex.ru/docs/checkout-api/#idempotentnost
                          +18
                          продуктово было согласовано, что недоставка нотификаций лучше, чем их дублирование

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

                            +3
                            Ну я бы поспорил. Если брать такси, то допустим мы можем потерять 1% нотификаций, при этом за рулём такси у нас пока что не робот и если что он позвонит клиенту, или дать ему кнопку повторной нотификации. Так же клиент обычно ждет такси и сам поглядывает где там оно и ему эти уведомления тоже не критичны, получается что проблемы это если и создает, то не очень сильно. Зато получить 100 дублей раз в минуту, часа в 3 ночи, когда уже лёг спать.
                              +1
                              Если сообщение не жалко потерять, то его не стоит и посылать. Конкретно с ЯТ-ом меня подбешивает получать по 4 нотификации после каждой поездки. И оценку поставь, и выписку глянь, и от банка уведомление о списании почитай, а списаний было аж 2 штуки.
                                –1
                                Зачем вызывать такси, если уже лег спать?
                                  +2
                                  Зачем вызывать такси, если уже лег спать?

                                  Гостям, чтобы они уже убрались добухивать у себя дома.
                                    0
                                    Дверь они тоже сами за собой запрут, как уйдут?) Ну, в смысле, я имею в виду, что ваш кейс очень синтетический.
                                      0
                                      Дверь они тоже сами за собой запрут, как уйдут?)

                                      А что у вас она автоматически не захлапывается?
                                        0
                                        Нет, она захлопывается, но потянув за ручку извне ее можно открыть.
                                    0
                                    Вдруг вам во сне захотелось куда-то поехать.
                                    Ну а если серьезно, то уведомления могут приходить и после поездки, очевидно же.
                                  +3
                                  Хорошее замечание, спасибо!

                                  В простейшей реализации выбор стоит не между 0 и 2 SMS, а между 0 и n (например, 100) SMS. Я сталкивался на одном из прошлых мест работы с тем, как пользователю приходят десятки сообщений из-за подобной логики.

                                  Можно пытаться запоминать в базе число попыток отправить SMS и применять at least once семантику на первые 3 попытки, а потом отбрасывать. Но здесь есть схожая проблема: что если произошел таймаут при запоминании факта попытки отправки SMS: выбирать at least once, или at most once семантику.

                                  Обычно, семантика выбирается в зависимости от важности конкретного типа SMS.
                                    0
                                    Да уж, ждём 100% покрытие планеты ультранадежным и ультраскоростным соединением. Все беды от плохой связи…
                                      +3
                                      Нет. Беда скорее всего в отсутствие идемпотентности в отправке СМС.
                                        0
                                        Потому что это чужой сервис. Его разработчики могли бы сделать его идемпотентным, но им было лень. Тем более, что каждая отправка — списание со счёта клиента, то есть исправлять эту проблему не особо выгодно.
                                      +1
                                      Как Apache Kafka решила эту проблему? Они гарантируют exactly once semantics с версии 0.11
                                        +2
                                        Как то так
                                        На самом деле введением кучи ограничений на consumer-ов и небольшим количеством сносок к термину «exactly once» (exactly once гаранитируется только на ветке producer->kafka и только в смысле «в кафке не будет дублирования сообщений» при этом не гарантируется что при отправке следующего дубля producer узнает что отправляет дубль)
                                          +5
                                          Exactly once требует согласованности продюсера и консьюмера. Если внешний апи на (на который происходит отправка смс) не предоставляет возможности передать локальный айдишник чтобы случае сбоя или обрыва связи проверить по нему произошла ли обработка запроса или нет то exactly onсe семантику в этом случае никак не получить. А kаfka гарантирует exactly once семантику только между своими же нодами.
                                          0
                                          А что мешает снимать задачу после нажатия водителем кнопки «клиент принят на борт»? То есть задача может долбиться в сервис отправки SMS и обламываться, но потом приходит уведомление от водителя и задача снимается. А в статистику падает, что отправка уведомления лагала и глючила и с нею уже потом разбираются разработчики.
                                        –26

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

                                          +4
                                          Ух ты вау, в институтах учат рисовать на блок-схеме сбои сети и таймауты?
                                            –10
                                            В институте учат, что приведенные вами примеры есть ветки алгоритмов, про которые Вася забыл или недодумал. Про идемпотентность там же учат после того, как квадратики усвоены.
                                          +2
                                          Да уж, век живи…
                                          Вот, когда читал доки мелкомягких по REST API, особо не обратил внимание на принцип идемпотентности, а зря. Казалось бы, ан нет…
                                            +5
                                            По-хорошему, Васе нужно было на нулевом шаге сообщить менеджерам, что нужен аналитик и, в идеале, архитектор, чтобы Васе, вместо экстренного гуглежа всего подряд после очередного бага, нужно было бы просто запрограммировать формализованную бизнес-логику =) Понятно, что это исключительно иллюстративно, однако не дай Кнут программисту с опытом меньше ~10 лет влететь на такую позицию. Да и с опытом лучше не надо.
                                              +4
                                              По-хорошему, Васе нужно было на нулевом шаге сообщить менеджерам, что нужен аналитик

                                              Если Вася всего лишь кодер — то да.
                                              Если он все же разработчик — аналитик тут не нужен. Тут задача не такая уж и сложная, чтобы квалифицированный разработчик сам не разрешил.
                                                –4
                                                Если вам нравится работать практически без ТЗ — это не значит, что это хорошая практика =)
                                                  0
                                                  >Если вам нравится работать

                                                  Ну очевидно, что прогер работающий только по разжеванному ТЗ, ценится меньше. А дальше уже каждому решать кем ему быть…
                                                    0

                                                    Очевидно, я был неправильно понят. Я не говорю, что хорошему прогеру всё нужно разжёвывать до уровня псевдокода. Но, тем не менее, он НЕ ДОЛЖЕН сам пытаться понять все требования к системе, потому что это, в общем-то, не его работа. Если программиста бросить на какую-то задачу, не сообщив НИЧЕГО хотя бы о требованиях к системе, то там прод будет с каждым обновлением падать. Это как сказать столяру: «А бахни-ка мне, голубчик, кухонный гарнитур», не уточнив, кухня на 6 квадратов или на 60. Бахнуть-то он, может, и бахнет, только не то, что надо.

                                                      0
                                                      Либо бахнет то что надо, но не в срок, т.к. понадобится дополнительное время на сбор требований и составление более менее адекватного ТЗ.
                                                        0
                                                        Именно. Девять женщин, конечно, ребёнка за месяц не родят, но всё же работа разных людей над разными составляющими задачи сокращает требуемое время и уменьшает вероятность возникновения ошибок. Условный Вася, по крайней мере, может посмотреть на ТЗ, составленное условным Петей, и свежим взглядом выловить какие-то неочевидные моменты.
                                              +1
                                              Мне кажется в плане решения проблем с идемпотентностью http и rest вносят большую путаницу чем rpc. С rpc все запросы по умолчанию неидемпотентны и это заставило бы Васю уже на этапе проектирования задуматься о повторных запросах из-за плохой связи. И с rpc такая проблема решается даже проще чем c http, ее можно вообще решить на уровне транспорта и упростить бизнес-логику. С rpc можно использовать вебсокеты которые в отличие от http имеют строгую очередность — запросы приходят на сервер точно в таком же порядке в каком были отправлены от клиента. А это значит что нам не нужно хранить день или сколько там idempotency key от всех http-запросов клиента а достаточно хранить только айдишник последнего запроса клиента и потом когда клиент после разрыва связи снова соединится с сервером он должен в первую очередь загрузить этот айдишник и проверить совпадает ли он с последним отправленным запросом и если совпадает тогда отменить запросы которые ждут повторной отправки. И все это можно решить на уровне транспорта и автоматически ретраить запросы при обрыве связи а на уровне бизнес-логики будет просто отправка запроса и отображение крутилки пока этот запрос не выполнится
                                                +1
                                                У нас был такой протокол, только на обычных сокетах — там тоже такой нетривиальный код получался с многопоточностью. Помнится одну ошибку ловили неделю на стенде — терялось сообщение раз в пару дней.
                                                +1
                                                1 приложение при отправке запроса генерит уникальный ключ
                                                2 Приложение получает ответ-подтверждение запроса (в ответе содержится уникальный ключ)
                                                Если ответ не получен — повторяем запрос, пока не получим ответ(естетвенно обрабатываем ошибки таймауты и т.п. чтобы пользователь не озверел от ожидания)
                                                3 показывает пользователю что заказ принят
                                                4 Получив ответ посылает запрос об успешном получении ответа с заказом
                                                5 сервер считает заказ принятым только если от приложения пришел запрос об успешном получении ответа
                                                шаги 4 -5 могут идти фоново, пользователь думает что все в порядке
                                                приложение периодичеки обновляет информацию о заказе
                                                если шаг 4 так и не прошел до сервера, то через 1-2 минуты статус заказ меняется на ошибку(очень маловероятно). Если пользователь закрыл приложение а заказ не потвердился — звоним ему, пишем смс
                                                  +5
                                                  Кажется, вы пытаетесь решить задачу двух генералов :)

                                                  Здесь та же проблема: на шаге 5 сервер мог получить подтверждение о получении заказа, но клиент об этом не узнает (не получив ответ). Если тогда через 1-2 минуты клиент покажет ошибку — пользователь подумает, что такси заказать не удалось, но машина неожиданно приедет.
                                                    –11
                                                    вероятность 0,0001% считаю допустимой
                                                      +12
                                                      При 10.000.000 заказов в месяц это 1000 «обманутых» клиентов.
                                                      Если каждый из них позвонит в техподдержку и проматерится всего лишь 3 минуты, нагрузка на колл-центр будет 1000*3/60 = 50 часов.
                                                      Процентов 20 из этих клиентов рано или поздно оставит в интернетах комментарий в стиле «вот уроды», процентов 5 не поленится поставить приложению минимальную отметку. Счастливчики, попавшие на этот баг второй раз, 100% уйдут к конкурентам.
                                                        –2
                                                        при 10 млн заказов 50 часов работы колцентра это мизер
                                                        а нарвавшихся 2 раза будет 10 из 10 млн
                                                        если они такие везунчики то уйдя к конкурентам нарвутся итам на что-нибудь
                                                          +8

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

                                                            +1
                                                            «Если они такие везунчики» — напомнило: наугад выбирает из середины стопки десяток дел:
                                                            — Этих уволить!!!
                                                            — Но за что???
                                                            — Не люблю неудачников…
                                                        –2
                                                        >вы пытаетесь решить задачу двух генералов

                                                        Сначала хипсторы возьмут неправильную технологию (stateless REST), потом придумывают умные слова почему задачу нельзя решить.

                                                        Сколько десятилетий протоколу TCP? Ну да, немодно это… зато будет работать и без 0,0001% ошибок.
                                                          +2
                                                          В мобильных сетях — нет, не будет. Именно из-за неразрешимости задачи двух генералов. Соединение однажды просто порвётся — и вы не будете знать, дошло ли последнее сообщение до другой стороны.
                                                            –2
                                                            Предположу что mspain под неправильной технологией stateless REST подразумевал http протокол на котором задача друх генералов «решается» сложнее. С вебсокетами или tcp если серверу нужно узнать дошел ли его ответ клиенту то он просто отправит запрос клиенту по тому же соединению и (а tcp будет ретраить отправку) и после получения подтверждения поймет что его ответ дошел (так как сообщения строго упорядочены). А с http упорядоченность запросов не гарантируется — запросы на сервер могут придти не в том порядке в котором были отправлены клиентом (то есть могут пойти по разным tcp-соединениям), ну и также в случае с браузером там канал только в одну сторону (от клиента к серверу) и сервер просто не может самостоятельно отправить запрос клиенту по http
                                                              +2
                                                              После разрыва TCP-соединения сервер тоже не сможет отправить клиенту уточняющий запрос.
                                                                –1
                                                                Хорошо, что я не математик, а программист. Поэтому у меня задача решаема.

                                                                Мобильное приложение открыло TCP соединение, в нем отправило ID клиента, ID поездки (чтобы можно было несколько машин вызывать), адрес. [в СУБД начинается транзакция]
                                                                Сервер шлёт «stage1»
                                                                клиент отвечает «stage1confirmed» [клиент начинает писать «бронирование авто»]
                                                                сервер шлёт «stage2»
                                                                клиент отвечает «stage2confirmed»
                                                                клиент отправляет FIN, если сервер его получает, СУБД делает COMMIT, если нет, то ROLLBACK. Если клиент не ловит финальный ACK, мобильное приложение через несколько секунд делает обычный REST запрос статуса заказа (это те самые 0.000001%). Они тоже учтены.
                                                                [мобильное приложение пишет «машина выехала»]

                                                                >После разрыва TCP-соединения сервер тоже не сможет отправить клиенту уточняющий запрос.

                                                                Когда заказ создан, клиент poll-ит сервер через rest, серверу ничего не надо отправлять.
                                                                  0
                                                                  Если клиент не ловит финальный ACK, мобильное приложение через несколько секунд делает обычный REST запрос статуса заказа (это те самые 0.000001%)

                                                                  А если нет связи?


                                                                  Давайте посмотрим примеры из обсуждаемой статьи. Пользователь нажал "заказать такси" и тут пропала связь. Вы будете разблокировать кнопку или нет?


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

                                                                    –3
                                                                    mayorovp, в Яндексе такие стесняшки, что боятся написать клиенту правду?! «Дорогой друг, нету ножек, нету варенья. Чтобы вызвать наше хайтек онлайн такси надо иметь интернет в течение хотя бы PING*5 секунд. Можем предложить архаичный сервис с бабушками на коллцентре, тел для связи ХХХ-ЙЙЙЙ-УУУУ. А ещё, на яндекс-маркет есть клёвые телефоны с вооОООООооот такой антенной! Ссылка откроется автоматически через 42 милисекунды. С уважением команда Яндекс!».

                                                                    100% надежный детект того, что «что-то пошло не так» я предложил.

                                                                    KYuri, Вы серьёзно? БД колом встанет

                                                                    Вы серьёзно используете СУБД которая встаёт колом от 3.9 транзацкий в сек? 10000000/30/84600

                                                                    >Вы нигде не использовали особые фичи TCP

                                                                    У TCP нет «особых фич», потому что это надстройка над IP.
                                                                    Хипсторам конечно по душе над statefull TCP громоздить stateless REST, потом костыльнуть поверх него statefull протокол. Приправить умными словечками, брейнстормить, пучить, таращить, попивать смузи,
                                                                    писать статьи как правильно строить хайлоад.
                                                                      +3
                                                                      Вы серьёзно используете СУБД которая встаёт колом от 3.9 транзацкий в сек?
                                                                      БД встаёт колом не от требуемой скорости обработки транзакций, а от наличия взаимоблокировок.
                                                                      Каждая взаимная блокировка увеличивает шанс, что произойдёт следующая — происходит лавинообразный рост блокировок и БД встаёт колом.
                                                                      Примерно так:
                                                                      image
                                                                      image

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

                                                                      А то, что Вы продолжаете утверждать, что «при 3.9 транзакций в секунду» (3.9 — это средняя нагрузка, при допущении, что «десятки миллионов» = «десять миллионов», а не, допустим, «девяносто миллионов»; пиковая же нагрузка может быть и на два, а то и на три порядка больше) можно на всё забить, говорит лишь о том, что практического опыта у Вас нет.
                                                                        –5
                                                                        БД встаёт колом не от требуемой скорости обработки транзакций, а от наличия взаимоблокировок


                                                                        Коли взялся поучать, следи за своими словами. У пряморукого ДБА база колом встаёт как раз от количества транзакций, большим количеством которых реляционные СУБД похвастать не могут.

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

                                                                        Подрастёшь, узнаешь, что в ORM например вообще не управляешь транзакциями СУБД и ничего, живёт как-то народ.

                                                                        Совсем подрастёшь, узнаешь, что бывают штуки навроде JTA XA Transactions, транзакции размазанные на большое число бэкендов. И ничего, живут как-то.
                                                                          0
                                                                          С временем нахождения в транзакции это связано очень слабо, а с косорукостью очень сильно.

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


                                                                          Подрастёшь, узнаешь, что в ORM например вообще не управляешь транзакциями СУБД и ничего, живёт как-то народ.

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

                                                                            +1
                                                                            У пряморукого ДБА...
                                                                            это связано… с косорукостью очень сильно

                                                                            К какой категории своё предложение (стартануть транзакцию, и уйти в сетевой обмен) отнесёте?
                                                                      +3

                                                                      [в СУБД начинается транзакция]
                                                                      Сервер шлёт…
                                                                      клиент отвечает…
                                                                      сервер шлёт…
                                                                      клиент отвечает…
                                                                      клиент отправляет FIN, если сервер его получает, СУБД делает COMMIT, если нет, то ROLLBACK.
                                                                      Вы серьёзно? Держать активную транзакцию всё время сетевого обмена? Не боитесь, что БД колом встанет?
                                                                      –2
                                                                      Хорошо, что я не математик, а программист. Поэтому у меня задача решаема.

                                                                      Открыли TCP соединение, в нем отправили свой ID клиента, ID поездки (чтобы можно было несколько машин вызывать), адрес. В СУБД начинается транзакция.
                                                                      Сервер шлёт «stage1»
                                                                      клиент отвечает «stage1confirmed» [с этого момента у клиента начинает писать «начато бронирование авто»]
                                                                      сервер шлёт «stage2» [с этого момента у клиента начинает писать «авто в пути»]
                                                                      клиент отвечает «stage2confirmed»
                                                                      с клиента должен прилететь FIN, если это происходит СУБД делает COMMIT, если нет, то ROLLBACK. Финальный ACK мобильному приложению уже не роляет, в любом случае процесс пошёл.

                                                                      Слово математикам, что может пойти не так?

                                                                      >После разрыва TCP-соединения сервер тоже не сможет отправить клиенту уточняющий запрос

                                                                      Для обновлений статуса (где едет машина) REST уже вполне уместен.
                                                              +1
                                                              1. приложение при отправке запроса генерит уникальный ключ. Отправляет запрос. Показываем пользователю крутилку в статусе заказа
                                                              2. Приложение получает ответ-подтверждение запроса (в ответе содержится уникальный ключ), крутилка превращается в «заказано»
                                                              3. Если сервер не ответил за таймаут, отправляем с темже ключом.
                                                              4. При получении запроса сервер проверяет, обработано ли сообщение с таким ключом. Если обработано — возвращает результат обработки, если не обработано — обрабатывает. Обработка — транзакционная, при попытке параллельной обработки один запрос отрабатывает, воторой отваливается на юниккей, в обоих случаях приложению возвращается результат обработки (в данном случае — успешно)
                                                                0
                                                                не могу понять, а в чем отличие от описанного в статье?
                                                              +4
                                                              все намного проще.

                                                              Клиент выгружает не заказ, а заявку на создание заказа.

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

                                                              Вы недоотделили клиента от сервера.
                                                                0
                                                                Есть следующий кейс:
                                                                1. клиент отправляет заявку, заявка создается, но ответ от сервера не был получен, интернет вообще пропал на какое-то время
                                                                2. проходит какое-то время
                                                                3. сервер начинает шедулить заявки: искать дубли заявок и выбирать исполнителя.
                                                                4. сервер назначает водителя
                                                                5. у клиента отвисает интернет и он повторяет исходный запрос создания заявки
                                                                6. создается дублирующая заявка
                                                                7. по этой дублирующей заявке позже приезжает второе такси

                                                                Не понимаю идеи, чем заявки на создание заказа помогают?
                                                                  0
                                                                  тем что это отдельный процессы

                                                                  7. по этой дублирующей заявке позже приезжает второе такси


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

                                                                  Второе такси приедет если вы намеренно отключите все проверки.

                                                                  В вашем же варианте куча каких-то ухищрений и странных терминов.

                                                                    0
                                                                    на любое количество одинаковых заявок создается один заказ

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

                                                                      Куда проще-то.

                                                                      Такси прекрасно приезжали на вызов еще до этих ваших интернетов с ключами импотентности.
                                                                        +3
                                                                        Понял, этот подход работает, пока не нужно позволять заказывать несколько такси одновременно. В статье есть раздел про мультизаказ.
                                                                          +2

                                                                          вобщет это работает и работало всегда.


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


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


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

                                                                            0
                                                                            Существует проблема не вызова одной-двух машин одного такси на один адрес, вызов несколько машин разных служб такси, из той серии, кто раньше приехал, тот и заработал.
                                                                              +1
                                                                              существует проблема отображения бизнес-логики на код.
                                                                                0
                                                                                есть ощущение как пользователя, что все как то не очень в яндексе с этим делом.
                                                                              0
                                                                              Case 1:
                                                                              Клиент заказал такси, а затем кто-то из его же дома и квартиры попросил его заказать еще одну машину. А потом еще одну. Абсолютно нормальная ситуация, которая иногда случается. По-крайней мере, у меня. Система ругается и говорит, что это невозможная ситуация и приезжает одна машина.

                                                                              Case 2:
                                                                              Человек встречает гостей и в конце вечера трое из них независимо друг от друга заказывают такси. Приезжает одно такси на всех, потому что у всех один адрес.
                                                                                –3
                                                                                вероятно вы не понимаете каких-то базовых вещей.

                                                                                Прием заявки это отдельная маленькая операция — просто принять как есть и положить в базу.

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

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

                                                                                  Конечно, разъезжаются люди с вечеринки, например.
                                                                                    0
                                                                                    Или с концерта/футбола (для большей масштабности).
                                                                                    0
                                                                                    Case 1 (несколько машин от одного клиента)
                                                                                    Отображать клиенту что одна машина заказана, перед оптправлением заявки спрашивать: хотите заказать 2-ю машину, отправлять на сервер заявку на заказ _второй_ машины.

                                                                                    Case 2
                                                                                    Как уже было сказано, заявка идентифицируется id клиента и адресом, три человека получат три разных машины.
                                                                                  –2

                                                                                  order handling можно разбить на order negotiation и order fulfillment. Таким образом, заказ на третью машину будет принят, но его можно результировать в нотификацию о лимите вместо выполнения.

                                                                                    +2
                                                                                    Поддерживаю вашего оппонента. Мне тоже показалось, что клиент берёт на себя слишком много. Сервер — это полное состояние бизнес-логики, а клиент — лишь её отображение. Клиент «нажимает кнопки» — отправляет заявки на действия, сервер проверяет возможно ли выполнение такого действия в текущем состоянии, и если точно такой же заказ, пусть даже и как часть мультизаказа, есть, то отвергает заявку и даже не шлёт клиенту, а просто помещает в список отвергнутых заявок клиента, который клиент, если хочет может запросить, т.к. он только отображатор.
                                                                                  0

                                                                                  Разве сервер не всегда знает, получил ли клиент ответ? Разве нельзя здесь применять транзакции в БД?


                                                                                  Например:


                                                                                  1. клиент отправляет заказ
                                                                                  2. сервер начинает транзакцию
                                                                                  3. если ответ клиент не получает (отпадает по таймауту), транзакция откатывается
                                                                                  4. если клиент ответ получил, транзакция комитится
                                                                                    +2
                                                                                    Нет, не знает. Даже если ждать подтверждения получения последнего байта пакета (TCP позволяет) — в случае ошибки сервер не будет знать в какой момент разорвалась связь: когда ответ шел к клиенту, или когда пакет ACK шёл к серверу.

                                                                                    И проблема тут фундаментальная: математически доказано что никакое конечное число подтверждений не может дать гарантии, см. задачу о двух генералах.
                                                                                0
                                                                                А если как в TCP сделать? Интернет работает, никто не жалуется.
                                                                                Клиент заказ N1 -> Сервер подтверждение N1 -> Клиент адрес для N1 -> Сервер номер такси для N1 -> Клиент отобразил заказ.
                                                                                Первые два этапа установление соединения, только потом данные или разрыв соединения по таймауту или инициативе клиента, сервера. Клиент может одновременно открыть хоть 1, 2… 10 заказов ничего страшного. Все заказы уникальны, сервер и клиент знают их номера, договариваются на первых двух этапах.
                                                                                  +1
                                                                                  Интересная аналогия.
                                                                                  Если сравнивать операцию создания заказа с открытием TCP соединения, то в случае ее неидемпотентности либо приезжает лишнее такси, либо создается лишнее соединение. Первое намного критичнее второго.
                                                                                  Если сравнивать операцию изменения заказа с посылкой данных по TCP соединению, то у TCP есть sequence numbers (в какой-то степени аналогия с версионированием списка заказов и ключа идемпотентности) для защиты от дублирования, переупорядочивания и тп.
                                                                                    0
                                                                                    либо создается лишнее соединение
                                                                                    Исключено т.к. соединение либо создается, либо нет, и такси ищется только один раз. Пока клиент не получит номер такси, он номер заказа не меняет. Только если решил сделать ещё один заказ, установить два соединения.
                                                                                    Клиент заказ N1 -> Сервер подтверждение N1 -> Клиент адрес для N1 -> Сервер (соединение установлено) Ищем такси -> Клиент (соединение установлено) отобразил заказ, номер такси.

                                                                                    Только, если клиент действительно хочет сделать два заказа, клиент работает с двумя номерами заказа. Повторюсь, пока клиент не получит номер такси, он номер заказа не меняет.
                                                                                    Для защиты еще можно ввести команду список заказов, но это, возможно, лишнее. А вот PING заказа на сервере, каждые 1-2 минуты клёвское дело, тем более статус заказа в любом случае мониторить.
                                                                                • НЛО прилетело и опубликовало эту надпись здесь
                                                                                    –1
                                                                                    Ввести такое понятие как кеш заявок — это строка из суммы кодов всех текущих заявок.
                                                                                    Он(кеш) отправляется при каждой посылке пакета в обе стороны, клиенту для установки, на сервер для контроля.

                                                                                    при п1 — кеш = ""
                                                                                    при п5 у клиента кеш = "" а на сайте «заявка1фывфвыфв»
                                                                                    из за разности кеша заявка 5 не выполняется и клиенту отсылается текущее состояние с текущем кешом «заявка1фывфвыфв»
                                                                                  0
                                                                                  Сервер атомарно с изменением увеличивает версию при любых изменениях заказов
                                                                                  И если это состояние заказов (версия) расходится с тем, что хранится на сервере
                                                                                  Нельзя ли в таком случае вместе с запросом отправлять с клиента просто хеш от известного списка заказов, а на сервере сравнивать с хешем актуального серверного списка.
                                                                                    +1
                                                                                    Спасибо! Я дописал в этот абзац следующее предложение:
                                                                                    Также стоит отметить, что версия может быть как числом (номером последнего изменения), так и хэшом от списка заказов: так, например, работает параметр `fingerprint` в Google Cloud API для изменения тегов инстансов.
                                                                                      0
                                                                                      Направление мысли Васи верное, но он не до конца понял идею идемпотентности, прикрутив подпорку с версионированием. Версионирование — это концепция из неидемпотентного мира. В идемпотентном API клиент всегда шлет список активных (по его мнению заказов), а сервер реагирует при обнаружении несоответствия его БД присланной хотелке. Это реализует все сценарии: добавление, удаление и изменение, в одном единственном методе API: sync(orders)
                                                                                        +1
                                                                                        Возможно, что версия тут — это такой краткий вариант описания списка активных заказов?

                                                                                        Это позволяет не пересылать весь список заказов обратно на сервер и не сравнивать его с текущим положением дел в БД. Если БД при каждом обновлении инфы конкретного клиента увеличивает версию, то это гарантирует, что при отличии версии пришёл старый запрос.
                                                                                          0
                                                                                          Именно: версия/фингерпринт по сути делает то же самое только проще/удобнее/экономнее.
                                                                                      0
                                                                                      … прямо мини-блокчейн «на двоих»! (только логика не «большинство узлов», а «сервер главнее»)
                                                                                      0
                                                                                      Не пойму почему в случае со сценарием 2, когда отменили такси, оно приедет… Ведь запись удалили из БД.
                                                                                        0
                                                                                        На шаге 3 клиент делает повторный запрос заказа такси, так как на шаге 1 клиент не получил ответа от сервера. И повторный запрос успешно выполняется, что приводит к приезду такси.
                                                                                          0

                                                                                          Вы наверно про другую ошибку, когда приехало 2 такси. Я говорю про случай, когда клиент сделал заказ, получил ошибку, потом или он через пуш или диспетчер отменили заказ, он удалился из бд а клиент сделал новый заказ. Почему приехало отменённое такси если записи в бд нету?

                                                                                            0

                                                                                            сценарий 2 сюда скопирую для удобства:


                                                                                            1. клиент отправляет запрос на создание заказа, заказ создается, но мобильная сеть лагает, и клиент не получает ответ об успешном создании заказа;
                                                                                            2. диспетчер, либо сам клиент через пуш по какой-то причине отменяет заказ: отмена заказа сделана как удаление строки из таблицы базы данных;
                                                                                            3. клиент шлет повторный запрос на создание заказа: запрос успешно выполняется и создается еще один заказ, так как ключ идемпотентности, хранившийся в прошлом заказе, больше не существует в таблице.

                                                                                            Почему приехало отменённое такси если записи в бд нету

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

                                                                                              +1
                                                                                              Клиент — это приложение, а не пользователь.
                                                                                              То есть, на шаге 3 приложение отправило повторный запрос и создало еще один заказ.
                                                                                              Т.к. клиент (приложение) не в курсе того, что заказ был создан на шаге 1.
                                                                                                0

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

                                                                                          +2
                                                                                          Каждая команда (на клиенте) имеет свой уникальный уид, и вот как делаем:

                                                                                          — Отправка заказа на сервер: клиентское приложение стучит на сервер до тех пор, пока не сработает заранее определенный таймаут или не получит нужный ответ
                                                                                          — На сервере проверяем есть ли уже команда с таким уид, если нет, то выполняем
                                                                                          — Если это команда на создание заказа, выставляем ее статус как пока не подтвержденная
                                                                                          — Клиентское приложение спрашивает о состоянии заказа с уид: его просит подтвердить заказ (приложение должно само отправить true например). Заказ помечается как созданный и по нему дальше производятся действия.

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

                                                                                          Для клиента это будет выглядеть как спиннер, который «подождите, проводится операция», который потом меняется или на «таймаут, нажмите снова» или на «ок, выполнено».

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

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

                                                                                            Уникальный уид у каждой команды — аналог ключа идемпотентности из статьи.


                                                                                            — На сервере проверяем есть ли уже команда с таким уид, если нет, то выполняем

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


                                                                                            — Клиентское приложение спрашивает о состоянии заказа с уид: его просит подтвердить заказ (приложение должно само отправить true например). Заказ помечается как созданный и по нему дальше производятся действия.

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

                                                                                              0
                                                                                              Для этого нужна распределенная транзакция, что далеко нетривиально.

                                                                                              Использовать key-value хранилище, умеющее в шардинг, их много на выбор.

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

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

                                                                                              Все сценарии можно проработать.
                                                                                                +1
                                                                                                Использовать key-value хранилище, умеющее в шардинг, их много на выбор.

                                                                                                Чем шардинг тут поможет? Как вы атомарно запишите в две разные субд? Либо вероятностный 2PC использовать, либо через очередь добиваться eventual consistency, либо подобные варианты, это сильно сложнее описанного в статье.

                                                                                            0
                                                                                            Но мультизаказ полностью ломал Васину схему защиты от дублей
                                                                                            А разве нельзя было сделать так: раз приложение знает, что у же есть какой-то заказ, а пользователь хочет сделать второй, то этот самый пользователь указывает: «да, я хочу вторую машину». Приложение заказа такси шлёт в api дополнительный параметр: это дополнительная вторая машина &additional_taxi=2.
                                                                                            Бэкенд делает такую блокировку:
                                                                                            UPDATE active_orders SET n=2 WHERE user_id={user_id} AND n=1;
                                                                                            И если всё ок, то делает второй заказ. Нужен третий? Делаем n=3. Четвертый? Да пожалуйста! Пока-что не вижу здесь никаких проблем.
                                                                                            И у нас больше нет проблемы с ключами идемпотентности, т.к. сразу с тех пор, как вы их внедрили, у вас кажется появилось еще больше проблем, чем было до этого.
                                                                                            Например вот:
                                                                                            Например, при повторном вызове идемпотентного API создания заказа — заказ не будет создаваться еще раз...
                                                                                            Вы придумали использовать уникальный ключ идемпотентности, который будет одинаковый для всех заказов этого клиента в течение 24 часов. То есть придумываете ID, который должен быть каждый раз уникальным, но он у вас не уникален. А затем вы боретесь с тем, что этот ваш неуникальный ключ всегда повторяется, и используете кучу костылей для исправления этой неуникальности.
                                                                                            … Вася предложил Феде генерировать новый ключ идемпотентности в таком случае. Но Федя объяснил, что тогда может быть дубль: при сетевой ошибке запроса создания заказа клиент не может знать, был ли действительно заказ создан.
                                                                                            Как по мне, это самая хорошая идея — генерировать новый ключ, но не при каждом чихе, а лишь когда сервер точно-точно ответил, что всё ок, заказ создан, и что теперь то можно клиенту наконец-то сгенерировать новый ключ для всех будущих обращений. А если связь лаганула, ответ от сервера не дошел до клиента, и клиент не знает, что заказ сделан, то при следующем возобновлении связи, либо каждые N секунд спрашивать у сервера: «Ну что там с моим ключем идемпотентности? Заказ в итоге создался? А то ответа от тебя не получил...» и в любом случае ответа сервера(да заказ создался/нет заказ не создался), сгенерировать новый ключ для будущих заказов.
                                                                                            А то вы сначала делаете уникальный ID, а затем мучаетесь от того, что этот ID неуникален следующие 24 часа, и снова вводите почти те же костыли что и вводили до этого.
                                                                                              0
                                                                                              Генерировать новый ключ, но не при каждом чихе, а лишь когда сервер точно-точно ответил, что всё ок, заказ создан, и что теперь то можно клиенту наконец-то сгенерировать новый ключ для всех будущих обращений.


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

                                                                                              Наличие разных уидов для разных команд позволит спокойно класть все это дело в очередь и пытаться доставить на сервер.
                                                                                                +2

                                                                                                Спасибо, схема с &additional_taxi={n} выглядит интересно. Подозреваю, что дубли и гонки могут быть в сценариях подобных такому:


                                                                                                1. пользовател создал заказ №1
                                                                                                2. пользователь отправляет запрос на создание заказа №2 с параметром &additional_taxi={2}
                                                                                                3. заказ создается, ответ от сервер не доходит до клиента, клиент начинает ретраить запрос
                                                                                                4. параллельно диспетчером/водителем отменяется заказ №1, что уменьшает n в таблице active_orders c 2 до 1
                                                                                                5. запрос на создание заказа №2 отретраился и успешно создал дубль

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

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

                                                                                                0

                                                                                                Здравствуйте. Спасибо за статью. С помощью какого интструмента можно эмулировать сетевую ошибку ?

                                                                                                  +1

                                                                                                  Здравствуйте.
                                                                                                  Во-первых, стоит эмулировать как медленные запросы, приводящие к таймаутам, так и просто быстрый connection refused.
                                                                                                  Во-вторых, для ручного эмулирования плохой сети на серверной машине я обычно использовал iptables, есть еще netem.
                                                                                                  В-третьих, в девелоперской консоли хрома можно сэмулировать медленное мобильное соединение.
                                                                                                  В-четвертых, можно протестировать один раз потерю пакетов, но код дописывается постоянно, баги возникают постоянно, и оно уже не тестируется. Есть chaos engineering: эмулировать сетевые ошибки (и не только: падения сервисов и тп) в продакшене регулярно. Например, есть chaos monkey и chaoskube.

                                                                                                    +1

                                                                                                    Статью однозначно в закладки, как и автора. Видно практика!

                                                                                                    0
                                                                                                    Подозреваю что описан ваш реальный опыт, так как попадался на двойной заказ такси с Uber Russia в декабре:
                                                                                                    Вызвал такси, через пару минут приложение перестало показывать водителя, подумал что водитель отменил, а я не заметил сообщения, заказал еще раз. В итоге в истории две поездки с разницей в несколько минут: первая показывает правильные адреса, но оборвалась на пол пути; вторая доехала куда надо, но в адресах показывает что заказ был с запада Москвы на юг Гонг Конга.
                                                                                                      0

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

                                                                                                      0
                                                                                                      Помню как «Федя» это волшебное слово повторял 10 раз в день(грустно вздыхая) и даже в викторинах загадывал. Давно это было, но все мучает вопрос — долгое ли время проблемы, описанные в статье, оставались проблемами?
                                                                                                        0

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

                                                                                                        +7

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


                                                                                                        Однако гуиды, хеши… блокчейн бы ещё сюда прикрутили. Всё, что тут надо — это отразить предметную область в ресурсах. При работе с активными заказами пользователь не "создаёт заказы", а "вызывает такси номер 1", "номер 2" и тд.


                                                                                                        PUT /taxi/1
                                                                                                        
                                                                                                        when \2019-01-01T01:00:00+03:00
                                                                                                        from \Москва, ул. Садовническая набережная 82с2
                                                                                                        to \Аэропорт Внуково

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


                                                                                                        PUT /taxi/2
                                                                                                        
                                                                                                        when \2019-01-01T02:00:00+03:00
                                                                                                        from \Аэропорт Внуково
                                                                                                        to \Москва, ул. Садовническая набережная 82с2
                                                                                                          0

                                                                                                          Пользователь вызвал такси (у него номер 1), заказ завершился, после этого пользователь еще раз вызвал такси (у него тоже номер 1). Как теперь поменять для самого первого заказа оценку, например? PUT /taxi/1 будет менять уже другой заказ.

                                                                                                            +1

                                                                                                            Это уже поездки:


                                                                                                            GET /trip
                                                                                                            
                                                                                                            /trip/34676374
                                                                                                                departure_at \2019-01-01T02:00:00+03:00
                                                                                                                arrived_at \2019-01-01T03:00:00+03:00
                                                                                                                from \Аэропорт Внуково
                                                                                                                to \Москва, ул. Садовническая набережная 81
                                                                                                                vote 3
                                                                                                            /trip/652455
                                                                                                                departure_at \2019-01-01T01:00:00+03:00
                                                                                                                arrived_at \2019-01-01T02:00:00+03:00
                                                                                                                from \Москва, ул. Садовническая набережная 82с2
                                                                                                                to \Аэропорт Внуково

                                                                                                            PUT /trip/652455
                                                                                                            
                                                                                                            vote 5

                                                                                                            Обратите внимание, что структура данных у них совсем разная.

                                                                                                            0
                                                                                                            Лучше всё-таки создавать заказы, а не вызывать такси. Потому что заказ и его номер — вещь интуитивно понятная, а такси и его номер — что-то странное.
                                                                                                              0
                                                                                                              Это не номер заказа. Это номер слота в рамках которого мы хотим получить заказ. В терминах многопоточности: это не номер задачи, а номер воркера, который эту задачу выполняет.
                                                                                                                0
                                                                                                                В таком случае у вас появляется новая сущность — «номер слота». Зачем?
                                                                                                                  0
                                                                                                                  Странный вопрос. Вся статья о том зачем всё это нужно.
                                                                                                                    0
                                                                                                                    Не увидел в статье ничего про «слоты». Про слоты только вы в комментарии написали.
                                                                                                                      0
                                                                                                                      Слоты отвечают на вопрос «Как?», а не «Зачем?».
                                                                                                                        0
                                                                                                                        На вопрос «как» отвечает ваше предложение использовать оператор PUT вместо POST.

                                                                                                                        Так всё-таки, зачем слоты? Что в них удобного? Чем плох генерируемый на клиенте номер заказа без возможности переиспользования?
                                                                                                                          +1
                                                                                                                          Может тем, что пользователь думает в терминах слотов, а не заказов. Для него это «первое такси», а зачастую и «единственное такси», а не «заказ номер 491857881».
                                                                                                                            0
                                                                                                                            Вы каких-то странных пользователей находите.
                                                                                                                              0
                                                                                                                              Скорее вам стоит внимательнее присмотреться к тому как пользователь взаимодействует с такси. В частности, попробуйте вспомнить номер последнего заказа.
                                                                                                                                0
                                                                                                                                А зачем пользователю видеть номер этого заказа?
                                                                                                                              0

                                                                                                                              Если я правильно вас понял, вы предлагаете переиспользовать номера "первое такси", "второе такси", но как тогда определять, какой номер у "очередного такси" и когда оно становится "первым такси". Допустим, первая поездка в аэропорт ещё идёт, вторая уже закончилась, а пользователь вызывает третье такси открывая приложение во второй раз — оно должно отображаться/запрашиваться как "второе" или "третье"?

                                                                                                                                0
                                                                                                                                Либо как второе, либо как третее с очисткой списка по завершении всех поездок.
                                                                                                              0
                                                                                                              А если клиенту показать всю кучу заказов, и пусть ручками удалит не нужные?
                                                                                                                0

                                                                                                                Пользователь может не заметить — он уже мог свернуть приложение, телефон мог сесть.

                                                                                                                0
                                                                                                                Приложение, не уведомляя пользователя, сразу сделало перезапрос и получило 404

                                                                                                                А зачем делать повторный запрос, не уведомляя пользователя? Таймаут — это, скорее всего, проблема с соединением, которую должен решить пользователь. Подойти к окну, включить вайфай. Если ответы так и не будут доходить, то рано или поздно об этом все равно придется сообщить пользователю. И лучше сделать это раньше, чтобы не заставлять его ждать впустую, не питать ложных надежд, не заставлять перезапускать приложение. Тогда бы и этой проблемы с появлением ошибки при успешном выполнении не возникло бы.
                                                                                                                Я просто хочу понять вашу логику. За пост спасибо, интересно.
                                                                                                                  +1

                                                                                                                  На масштабах Яндекс.Такси таймауты происходят много раз каждую секунду даже при взаимодействии между микросервисами: где-то у сетевиков роутер подлагнул, где-то на сервере базы данных обращение к диску тормозит из-за битого сектора, где-то CPU перегружен у машины и тд.
                                                                                                                  Если рассмотреть пользователя такси, то это обычно мобильное приложение. Мобильная сеть обычно ненадежна и имеет большой latency. Легко попасть в зону плохого покрытия, зайти в переход. Особенно проблема актуальна в городах/странах где плохо с 3G.


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

                                                                                                                    0
                                                                                                                    Это вы смотрите со стороны своего бэкенда. Миллионы пользователей, много мест, где возможны задержки, поэтому они случаются постоянно. Это понятно. А если смотреть со стороны одного отдельного клиента, то как часто ему приходится делать запросы повторно? Именно по вине задержек на бэкенде, а не сети.
                                                                                                                    Но основной мой вопрос про сеть. Допустим, на улице зима, я зашел погреться в подземный переход. Сеть ловит, но очень нестабильно. Хочу вызвать такси. Приложение будет бесконечно молча повторять запросы?
                                                                                                                      +1
                                                                                                                      то как часто ему приходится делать запросы повторно?

                                                                                                                      Часто. Очень часто.

                                                                                                                  –1

                                                                                                                  Более простое решение — не показывать сетевые ошибки в GUI. Если до сервера достучаться не удалось, отображать "заказ отправляется" и дать возможность отменить отправку заказа. В логике, соответственно, повторять идемпотентный запрос до получения ответа от сервера. Если пользователь отменил отправку заказа, отправить запрос отмены заказа, так же с ретраями.

                                                                                                                    0
                                                                                                                    Вася бы мог обучить ИИ, который бы учитывал особенности бизнеса, и все запросы клиентов пропускать через ИИ. Тем более что у Васи есть куча собранной информации, есть на чем учить. И далее Васе было бы проще менять логику.
                                                                                                                      –2
                                                                                                                      Я все же не совсем понимаю, зачем нужен этот наворот в случае такси? В случае кассы — понятно. Там может прийти несколько платежей и не ясно, так действительно хотели или это дубль. Ну так для этих случаев финансисты с древних времен используют всякие дополнительные идентификаторы транзакций, задолго до того как появился REST.

                                                                                                                      А вот случае такси, как такое возможно? Что мешает сделать уникальный ключ в таблице по клиенту, from, to, и доверься надежности БД? Если insert прошел — ок. Если нет — ошибка -дубликат. Про мульти заказ не понятно тоже. Если подразумевается заказ на разные адреса, это это просто два разных заказа. Если по одному маршруту — так это просто атрибут типа автомобиля в одном заказе (типа сколько человек надо перевезти). Можно чтобы мини бас приехал и всех забрал, а можно чтобы одна или несколько машин. Клиенту это параллельно. Ему важно сколько человек должны уехать.
                                                                                                                        0
                                                                                                                        А может помочь ситуации, если между клиентом и сервером гонять всегда полное состояние сущности «пользователь-заказы»? И пока в базе не закончится какой-либо длительный процесс, связанный с пользователем, не убирать флаг «in_progress» и таким образом никогда не разблокировать UI при текущем процессе?
                                                                                                                        — И, соответственно, до получения состояния с сервера по умолчанию считать, что «заказ» невозможен.
                                                                                                                          0

                                                                                                                          Полная блокировка UI может решать часть проблем, в статье это и описано:


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

                                                                                                                          Но есть и другие точки входа в изменение заказов:


                                                                                                                          1. водитель/диспетчер меняет/отменяет заказ
                                                                                                                          2. кнопки действий в пушах
                                                                                                                          3. второе устройство
                                                                                                                          –2
                                                                                                                          Норм статья, но читать и описывать «алгоритмы» исключительно plain/text не всегда идеальный вариант. Всё таки стажёру Васи неплохо бы освоить язык блок-схем. Интересно в ya документацию ведут? или лучшая документация это код?

                                                                                                                            +2
                                                                                                                            Вот интересно, идемпотентность в Яндекс.Таксометр API есть? :) Например, в методе «Изменить баланс водителя»? Ответ — нет
                                                                                                                              +2
                                                                                                                              Забавно, что версионность в урлах апи есть, но по факту, под v1 кроются разные API.
                                                                                                                                0
                                                                                                                                Прочёл статью, подумал, но понять не могу — почему нельзя всего лишь один шаг в клиентскую часть добавить — проверка созданного заказа? Если хочешь вторую машину — это либо дополнительный параметр запроса, либо вовсе другой метод. А идемпотентность в данном случае лишняя сущность, вызывающая усложнение.

                                                                                                                                В идеале просто добавить сразу в ответ сервера, например, хэш всех заказов, которые есть у клиента (чтобы все их параметры не передавать). Насколько я понимаю вашу идею — хэш в данном случае не замена ключу идемпотентности, поскольку в данном случае передаётся не всегда и только от сервера клиенту, но не наоборот. Хэш высчитывать из параметров заказа. Таким образом полную «локальную базу» иметь не надо. Достаточно сравнивать те заказы, которые мы локально создали и не отменили с тем, что есть на сервере.

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

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

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

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

                                                                                                                                И ещё один момент. Если клиент видит, что отправляемые им пакеты не получают ответа — рисовать ему «проблемы с сетью» и не делать лишнего это, вообще-то, хорошая практика (на мой взгляд). Тут самый смысл в том, чтобы уменьшить требования к сети, уменьшая объём передаваемых данных. Достаточно чтобы порог ошибок сети, при котором будет сообщаться о проблемах был очень высок, чтобы пользователи видели такое только в глухом подвале или в тайге.

                                                                                                                                P.S. Извините, с утра думается тяжело, могу тупить где-то, буду рад если объясните в каком моменте не прав.
                                                                                                                                  +1
                                                                                                                                  Допустим клиент отправляет множество запросов из-за плохой сети. В этом нет проблемы, если параметры заказа абсолютно идентичные — все, кроме первого запроса сервером тупо отклоняются.

                                                                                                                                  Что значит тупо отклоняются? Как сервер поймет что полученный запрос это повторная отправка одного и того же запроса (после обрыва сети) а не создание нового заказа? Смотрите тут проблема чисто логическая — если есть приложение где юзер может создавать что-то много раз неважно будут ли это поездки или задачи в таск-менеджере или комментарии — всегда возможна ситуация обрыва связи при котором возможны два варианта — а) запрос не успел дойти до сервера и тогда можно безопасно выполнять его повторную отправку б) запрос дошел до сервера и был обработан но обрыв связи произошел на обратном пути — и тогда повторная отправка уже будет небезопасна так как сервер посчитает что это был отдельный запрос и получим дубль. А клиент получив ошибку обрыва соединения никак не сможет понять был ли обрыв связи на пути к серверу или на обратном пути.
                                                                                                                                  И получается что в итоге возможны 3 варианта действий со стороны клиента — а) не пытаться ретраить запрос и понадеяться на успешную доставку записав в издержки редкие потери — это то соответствует семантике «at most once» в кафке или других брокерах сообщений б) при обрыве сети просто ретраить запрос, а в издержки записать возможные дубли при редких случаях когда связь обрывается на обратном пути — это соответсвует семантике «at least once» в) семантика «exactly once» — выполнять ретрай при обрыве связи но с каждым запросом передать серверу дополнительную информацию при котором сервер сможет различить был ли такой запрос обработан или нет чтобы тогда сервер отклонил этот запрос (либо возможен еще вариант когда перед ретраем происходит получение информации из сервера чтобы уже сам клиент без лишней отправки запроса отменил запросы которые успели обработаться)
                                                                                                                                  В зависимости от того что взять в качестве этой дополнительной информации возможны различные подходы:
                                                                                                                                  1) сгенерировать на клиенте рандомный айдишник для нового заказа (guid или uuid) чтобы при повторном запросе база данных выдала ошибку что такой заказ по такому айдишнику уже есть
                                                                                                                                  2) передавать такой же сгенерированный айдишник (ключ идемпотентности) но который не будет сохраняться в качестве primary id а будет храниться где-то в другом месте (но поскольку это накладные расходы то обычно хранят ограниченное время)
                                                                                                                                  3) сервер может не хранить айдишник для каждого запроса/заказа но тогда клиент должен с каждым запросом передавать список айдишников/хеш-заказов всех предыдущих заказов, еще есть вариант — передача версии списка заказов
                                                                                                                                  4) обобщая предыдущий способ — при передаче версии списка заказов или хеша происходит по сути выстраивание запросов клиента в очередь — то есть параллельно отправленные запросы клиента приведут к ошибке так как не совпадут версии/хеши. А в общем случае если у нас не одна сущность «заказы» а сложное crm-приложение с кучей сущностей то придется хранить по айдишнику/хешу версии на каждую таблицу либо можно хранить только один айдишник/хеш по всем запросам для одного юзера (точнее его сессии так как у юзера может быть залогинен с несколько устройств) а на клиенте выстраивать все запросы по всем сущностям в очередь
                                                                                                                                  И получается что минимизируя количество дополнительной информации которую должен хранить сервер мы приходим к тому что в случае ухудшаем производительность при использовании http-протокола — теперь нужно дожидаться результата предыдущего запроса http-запроса прежде чем отправить новый.
                                                                                                                                  Но это в случае http, в случае же вебсокетов — ситуация иная — вебсокеты гарантируют последовательность а значит что отправленные запросы на сервер в одном вебсокет-соединении будут приходить в том же порядке в котором был отправлены из клиента. А это значит что мы можем отправлять запросы сразу же не дожидаясь ответа предыдущего а в случае восстановления сети после обрыва связи достаточно получить от сервера айдишник последнего обработанного запроса и таким образом понять какие запросы успели обработаться и их следует удалить из очереди а какие нужно будет отправить повторно
                                                                                                                                    –2
                                                                                                                                    Как сервер поймет что полученный запрос это повторная отправка одного и того же запроса (после обрыва сети) а не создание нового заказа?
                                                                                                                                    Одному человеку нужно больше двух машин по одному и тому же маршруту? Окей, добавляем параметр с количеством машин в запрос, я же написал.

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

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

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

                                                                                                                                Самое читаемое