Лучшие практики для запуска облачной функции Firebase только один раз - PullRequest
0 голосов
/ 28 сентября 2018

Мне нужно запускать облачную функцию Firebase только один раз каждый раз, когда создается новый пользователь Firebase Auth.Я уже написал полностью работающую функцию, которая отправляет одно электронное письмо каждому пользователю, используя триггер onCreate.Функция отправляет приветственное электронное письмо и отслеживает некоторые аналитические данные, поэтому она не идемпотентна.

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

Я хочу знать, как лучше всего изменить поведение «хотя бы один раз» на «ровно один раз».

Что происходит сейчас:

  1. Регистрация нового пользователя "A".
  2. Google запускает "sendWelcomeEmail" для пользователя A.
  3. Google запускает «sendWelcomeEmail» для пользователя A AGAIN .

Каков наилучший способ запустить функцию всего один раз и отменить / пропустить любой другой вызов для того же пользователя?

1 Ответ

0 голосов
/ 30 января 2019

Я столкнулся с подобной проблемой, и нет простого решения.Я обнаружил, что для любого действия, использующего внешнюю систему, совершенно невозможно сделать такую ​​функцию идемпотентной.Я использую TypeScript и Firestore.

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

Я обнаружил, что существует 2 уровня этой проблемы:

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

Примерами такой интеграции являются:

  • отправка электронного письма
  • с подключением к платежной системе

1.Для неидемпотентной функции (простой сценарий)

async function isFirstRun(user: UserRecord) {
  return await admin.firestore().runTransaction(async transaction => {
    const userReference = admin.firestore().collection('users').doc(user.uid);

    const userData = await transaction.get(userReference) as any
    const emailSent = userData && userData.emailSent
    if (!emailSent) {
      transaction.set(userReference, { emailSent: true }, { merge: true })
      return true;
    } else {
      return false;
    }
  })
}

export const onUserCreated = functions.auth.user().onCreate(async (user, context) => {
  const shouldSendEmail = await isFirstRun(user);
  if (shouldSendEmail) {
    await sendWelcomeEmail(user)
  }
})

2.Для набора уже идемпотентных функций (сценарий реального случая)

Чтобы сделать эту работу с набором функций, которые уже идемпотентны, я переключился на систему массового обслуживания.Я помещаю действия в коллекцию и использую транзакции Firebase, чтобы «заблокировать» выполнение действия только для одной функции за раз.

Я постараюсь привести здесь минимальный пример.

Развертываниефункция обработчика действий

export const onActionAdded = functions.firestore
  .document('actions/{actionId}')
  .onCreate(async (actionSnapshot) => {
    const actionItem: ActionQueueItem = tryPickingNewAction(actionSnapshot)

    if (actionItem) {
      if (actionItem.type === "SEND_EMAIL") {
        await handleSendEmail(actionItem)
        await actionSnapshot.ref.update({ status: ActionQueueItemStatus.Finished } as ActionQueueItemStatusUpdate)
      } else {
        await handleOtherAction(actionItem)
      }
    }
  });

/** Returns the action if no other Function already started processing it */
function tryPickingNewAction(actionSnapshot: DocumentSnapshot): Promise<ActionQueueItem> {
  return admin.firestore().runTransaction(async transaction => {
    const actionItemSnapshot = await transaction.get(actionSnapshot.ref);
    const freshActionItem = actionItemSnapshot.data() as ActionQueueItem;

    if (freshActionItem.status === ActionQueueItemStatus.Todo) {
      // Take this action
      transaction.update(actionSnapshot.ref, { status: ActionQueueItemStatus.Processing } as ActionQueueItemStatusUpdate)
      return freshActionItem;
    } else {
      console.warn("Trying to process an item that is already being processed by other thread.");
      return null;
    }
  })
}

Выдвигать действия в коллекцию следующим образом

admin.firestore()
    .collection('actions')
    .add({
      created: new Date(),
      status: ActionQueueItemStatus.Todo,
      type: 'SEND_EMAIL',
      data: {...}
    })

Определения TypeScript

export enum ActionQueueItemStatus {
  Todo = "NEW",
  Processing = "PROCESSING",
  Finished = "FINISHED"
}

export interface ActionQueueItem {
  created: Date
  status: ActionQueueItemStatus
  type: 'SEND_EMAIL' | 'OTHER_ACTION'
  data: EmailActionData
}

export interface EmailActionData {
  subject: string,
  content: string,
  userEmail: string,
  userDisplayName: string
}

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

Если вы знаете более простой способ сделать это - пожалуйста, скажите мне, как:)

Удачи!

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