Я столкнулся с подобной проблемой, и нет простого решения.Я обнаружил, что для любого действия, использующего внешнюю систему, совершенно невозможно сделать такую функцию идемпотентной.Я использую TypeScript и Firestore.
Для решения этой проблемы вам нужно будет использовать Транзакции Firebase .Только с помощью транзакции вы сможете столкнуться с условиями гонки, которые возникают, когда функция запускается несколько раз, обычно в одно и то же время.
Я обнаружил, что существует 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
}
Возможно, вам придется настроить это с более богатыми статусами иих изменения, но этот подход должен работать для любого случая, и предоставленный код должен стать хорошей отправной точкой.Это также не включает механизм для повторного запуска неудачных действий, но их легко найти.
Если вы знаете более простой способ сделать это - пожалуйста, скажите мне, как:)
Удачи!