Повторный вызов асинхронных функций

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

Почему функции, управляемые событиями, не выполняются

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

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

  • Функция содержит ошибку, и среда выполнения выдает исключение.
  • Функция не может подключиться к конечной точке сервиса или время ожидания подключения истекло.
  • Функция намеренно вызывает исключение (например, когда параметр не проходит проверку).
  • Функция Node.js возвращает отклоненный объект Promise или передает обратный вызов со значением, отличным от null.

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

Семантика повторных попыток

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

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

Настройте повторные попытки в коде функции.

В Cloud Functions for Firebase можно включить повторные попытки в коде функции. Для этого в случае фонового события, например создания нового документа Firestore, задайте для параметра failurePolicy (первое поколение) или retry (второе поколение) значение true:

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

exports.docCreated = functions
  .runWith({
    // retry on failure
    failurePolicy: true,
  })
  .firestore.document("my-collection/{docId}")
  .onCreate((change, context) => {
    /* ... */
  });

2-е поколение

const { onDocumentCreated } = require("firebase-functions/firestore");

exports.docCreated = onDocumentCreated(
  {
    // retry on failure
    retry: true,
  },
  "my-collection/{docId}",
  (event) => {
    /* ... */
  },
);

Если задать для true значение, как показано ниже, функция будет повторять попытку в случае сбоя.

Период повторных попыток

Для функций второго поколения это окно повторных попыток истекает через 24 часа. Для функций первого поколения срок действия истекает через 7 дней. Cloud Functions повторяет попытки выполнить новые функции, управляемые событиями, используя стратегию экспоненциальной выдержки с увеличением задержки от 10 до 600 секунд. Это правило применяется к новым функциям при их первом развертывании. Оно не применяется задним числом к существующим функциям, которые были впервые развернуты до вступления в силу изменений, описанных в этом примечании к выпуску, даже если вы повторно развернете эти функции.

Рекомендации

В этом разделе описаны рекомендации по использованию повторных попыток.

Используйте повторные попытки для обработки временных ошибок

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

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

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

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

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

const eventAgeMs = Date.now() - Date.parse(event.timestamp);
const eventMaxAgeMs = 10000;
if (eventAgeMs > eventMaxAgeMs) {
  console.log(`Dropping event ${event} with age[ms]: ${eventAgeMs}`);
  callback();
  return;
}

Использование catch с Promises

Если для функции включены повторные попытки, любая необработанная ошибка приведет к повторной попытке. Убедитесь, что ваш код фиксирует все ошибки, которые не должны приводить к повторной попытке.

Вот пример того, что нужно сделать:

return doFooAsync().catch((err) => {
    if (isFatal(err)) {
        console.error(`Fatal error ${err}`);
    }
    return Promise.reject(err);
});

Как сделать идемпотентными функции на основе событий, которые можно повторить

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

  • Многие внешние API (например, Stripe) позволяют передавать ключ идемпотентности в качестве параметра. Если вы используете такой API, то в качестве ключа идемпотентности следует использовать идентификатор события.
  • Идемпотентность хорошо работает с доставкой по крайней мере один раз, поскольку делает повторную попытку безопасной. Поэтому общая рекомендация по написанию надежного кода – сочетать идемпотентность с повторными попытками.
  • Убедитесь, что ваш код является внутренне идемпотентным. Примеры:
    • Убедитесь, что мутации могут происходить несколько раз без изменения результата.
    • Запросите состояние базы данных в транзакции перед изменением состояния.
    • Убедитесь, что все побочные эффекты сами по себе идемпотентны.
  • Добавьте проверку транзакций вне функции, независимо от кода. Например, сохраняйте состояние, в котором записано, что данный идентификатор события уже обработан.
  • Обрабатывайте повторяющиеся вызовы функций вне диапазона. Например, можно настроить отдельный процесс очистки, который будет удалять данные, полученные в результате дублирующихся вызовов функций.

Настройте правила повторных попыток

В зависимости от потребностей функции вы можете настроить политику повторных попыток напрямую. Это позволит вам настроить любую комбинацию следующих параметров:

  • Сократите период повторных попыток с 7 дней до 10 минут.
  • Измените минимальное и максимальное время задержки для стратегии повторных попыток с экспоненциальной выдержкой.
  • Измените стратегию повторных попыток на немедленный повтор.
  • Настройте тему не доставленных сообщений.
  • Задайте максимальное и минимальное количество попыток доставки.

Чтобы настроить правила повторных попыток, выполните следующие действия:

  1. Напишите HTTP-функцию.
  2. Используйте Pub/Sub API, чтобы создать подписку Pub/Sub, указав URL функции в качестве цели.

Дополнительную информацию о настройке Pub/Sub можно найти в документации по обработке сбоев в Pub/Sub.