Очередь запросов

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

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

function saveBusStops(project_id, stops) {
  xhr({ 
    method: 'POST', 
    url: `${project_id}/bus_stops`, 
    json: {stops},
  }).then(() => 
    xhr({ url: `${project_id}/stats`})
      .then((body) => updateStats(body))
  );
}

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

const saveBusStops = debounce((project_id, stops) => {
  xhr({ 
    method: 'POST', 
    url: `${project_id}/bus_stops`, 
    json: {stops},
  }).then(() => 
    xhr({ url: `${project_id}/stats`})
      .then((body) => updateStats(body))
  );
}, 3000);

Некоторое время это работало нормально, но в какой-то момент у нас появились более крупные клиенты, для которых вызов /stats мог занять несколько секунд. Это означало, что иногда вызов /stats все еще выполнялся, когда были сохранены новые автобусные остановки, что приводило к нашей основной проблеме: перекрывающиеся вызовы API конфликтуют друг с другом. Пока мы запрашиваем базу данных в /stats В конечной точке данные изменяются из-за более позднего вызова /bus_stops, что может привести к тому, что конечная точка /stats в лучшем случае вернет неверные данные или в худшем случае произойдет сбой (например, из-за отсутствия / изменения идентификаторов).

Сериализуемые транзакции

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

  • PostgreSQL реализует сериализуемые транзакции, выдавая ошибку, если две транзакции конфликтуют. Затем вам нужно повторить эти транзакции. Если эти конфликты случаются часто (что для нас так и было), это значительно замедляет работу конечных точек.
  • Более медленные конечные точки означают, что проблема конфликтов усугубляется, поскольку больше вызовов конечных точек будут конфликтовать друг с другом.
  • Наши сериализуемые транзакции потребуют много блокировок. Когда замков слишком много, они становятся менее детализированными. Часто целые таблицы блокировались транзакцией, что приводило к их постоянному конфликту с другими транзакциями.

Другие проблемы

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

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

Поэтому нам нужно было придумать что-то получше.

Представляем xhrQueue

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

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

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

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

Вот как выглядит код:

function saveBusStops(project_id, stops) {
  window.xhrQueue.xhr({
    method: 'POST',
    url: `${project_id}/bus_stops`,
    json: {stops},
    ignorePreviousByUrl: true,
  });
  window.xhrQueue.xhr({
    url: `${project_id}/stats`,
    ignorePreviousByUrl: true,
  }, (error, response, body) => updateStats(body));
}

ignorePreviousByUrl удалит все существующие запросы в очереди с тем же URL-адресом.

Больше преимуществ

Когда у вас есть базовая модель очереди для запросов конечных точек, вы можете добавить некоторые оптимизации поверх нее. Один из них - указать, какие запросы могут выполняться параллельно, поскольку они не конфликтуют друг с другом. На данный момент у нас есть такая простая оптимизация: GET запросы могут выполняться параллельно, поскольку предполагается, что они не изменяют данные. Вы можете изменить это с помощью overrideQueueItemType: 'read' или 'write'.

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

Вывод

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

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

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

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