Это глава 22 моей книги «API». v2 будет содержать три новых раздела: «Паттерны API», «HTTP API и REST», «SDK и UI‑библиотеки». Если эта работа была для вас полезна, пожалуйста, оцените книгу на GitHub, Amazon или GoodReads. English version on Substack.

Одно из неприятных ограничений почти всех перечисленных в предыдущей главе технологий — это относительно невысокий размер сообщения. Наиболее проблематичная ситуация с push-уведомлениями: Google Firebase Messaging на момент написания настоящей главы разрешал сообщения не более 4000 байт. Но и в серверной разработке ограничения заметны: например, Amazon SQS лимитирует размер сообщения 256 килобайтами. При разработке webhook-ов вы рискуете быстро упереться в размеры тел сообщений, выставленных на веб-серверах партнёров (например, в nginx по умолчанию разрешены тела запросов не более одного мегабайта). Это приводит нас к необходимости сделать два технических выбора:

  • содержит ли тело сообщения все данные необходимые для его обработки, или только уведомляет о факте изменения состояния;

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

Рассмотрим на примере нашего кофейного API:

// Вариант 1: тело сообщения
// содержит все данные о заказе
POST /partner/webhook
Host: partners.host
{
  "event_id",
  "occurred_at",
  "order": {
    "id",
    "status",
    "recipe_id",
    "volume",
    // Все прочие детали заказа
    …
  }
}
// Вариант 2: тело сообщения
// содержит только информацию
// о самом событии
POST /partner/webhook
Host: partners.host
{
  "event_id",
  // Тип сообщения: нотификация
  // о появлении нового заказа
  "event_type": "new_order",
  "occurred_at",
  // Все поля данных, необходимые
  // для обращения за полным
  // состоянием. В нашем случае —
  // идентификатор заказа
  "order_id"
}
// При обработке сообщения,
// возможно, отложенной,
// партнёр должен обратиться
// к нашему API
GET /v1/orders/{id}
→
{ /* все детали заказа */ }
// Вариант 3: мы уведомляем
// партнёра, что его реакции
// ожидают три новых заказа
POST /partner/webhook
Host: partners.host
{
  // Здесь может быть версия
  // состояния системы или курсор
  "occurred_at",
  "pending_order_count":
    <число новых заказов>
}
// В ответ партнёр должен вызвать
// эндпойнт получения списка заказов
GET /v1/orders/pending
→
{
  "orders",
  "cursor"
}

Выбор подходящей модели зависит от предметной области (в частности, допустимых размерах тел сообщений) и того, каким образом партнёр будет обрабатывать сообщение. В нашем конкретном случае, когда партнёр должен каждый новый заказ обработать отдельно, при этом на один заказ не может приходить больше одного-двух уведомлений, естественным выбором является вариант 1 (если тело запроса не содержит никаких непредсказуемо больших данных) или 2. Третий подход будет естественным выбором, если:

  • API генерирует большое число сообщений об изменениях состояния на одну логическую сущность;

  • партнёров интересуют только наиболее свежие изменения;

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

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

Применение техник с отправкой только ограниченного набора данных помимо усложнения схемы взаимодействия и увеличения количества запросов имеет ещё один важный недостаток. Если в варианте 1 (сообщение содержит в себе все релевантные данные) мы можем рассчитывать на то, что возврат кода успеха подписчиком эквивалентен успешной обработке сообщения партнёром (что, вообще говоря, тоже не гарантировано, т.к. партнёр может использовать асинхронные схемы), то для вариантов 2 и 3 это заведомо не так: для обработки сообщений партнёр должен выполнить дополнительные действия, начиная с получения нужных данных о заказе. В этом случае нам необходимо иметь раздельные статусы — сообщение доставлено и сообщение обработано; в идеале, второе должно вытекать из логики работы API, т.е. сигналом о том, что сообщение обработано, является какое-то действие, совершаемое партнёром. В нашем кофейном примере это может быть перевод заказа партнёром из статуса "new" (заказ создан пользователем) в статус "accepted" или "rejected" (кофейня партнёра приняла или отклонила заказ). Тогда полный цикл обработки уведосления будет выглядеть так:

// Уведомляем партнёра о том,
// что его реакции
// ожидают три новых заказа
POST /partner/webhook
Host: partners.host
{
  "occurred_at",
  "pending_order_count":
    <число новых заказов>
}
// В ответ партнёр вызывает
// эндпойнт получения списка заказов
GET /v1/orders/pending
→
{
  "orders",
  "cursor"
}
// После того, как заказы обработаны,
// партнёр уведомляет нас об
// изменениях статуса
POST /v1/orders/bulk-status-change
{
  "status_changes": [{
    "order_id",
    "new_status": "accepted",
    // Иная релевантная информация,
    // например, время готовности
    …
  }, {
    "order_id",
    "new_status": "rejected",
    "reason"
  }, …]
}

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

Комментарии (0)