【问题标题】:Firebase functions trigger onDelete works sometimesFirebase 函数有时会触发 onDelete 工作
【发布时间】:2022-01-14 18:53:26
【问题描述】:

我有一个如下所示的 Firestore 数据库:

vehicle collection

orgs collection

users collection

一个集合用于车辆,一个用于组织,一个用于用户。在组织集合中,每个文档都有一个名为车辆的字段,其中包含该公司拥有的车辆集合中的车辆。 users 集合中的每个文档都有一个名为车辆的字段,其中包含该用户有权访问的所有车辆。在我的应用程序中,我可以删除整个组织(删除 orgs 集合中的文档)。然后我有负责其余部分的云功能。或者至少应该。

exports.deleteVehiclesInOrg = functions.firestore.document("/orgs/{orgId}").onDelete((snap) => {
 const deletedOrgVehicles = snap.data().vehicles;
 return deleteVehiclesInOrg(deletedOrgVehicles);
});

const deleteVehiclesInOrg = async(deletedVehicles: string[]) => {
 for (const vehicle of deletedVehicles) {
  await admin.firestore().doc(vehicles/${vehicle}).delete();
 }
  return null;
};

当车辆集合中的文档被删除时,上面的这个触发函数会从这个组织中删除所有触发这个函数的车辆:

const getIndexOfVehicleInUser = (vehicle: string,user: FirebaseFirestore.DocumentData) => {
 for (let i = 0; i < user.vehicles.length; i++) {
  if (user.vehicles[i].vehicleId === vehicle) {
   return I;
  }
 }return null;
};

const deleteVehiclefromUsers = async (uids: [{ userId: string }],vehicleId: string) => {
for (const user of uids) {
 const userSnap = await admin.firestore().doc(`users/${user.userId}`).get();
 const userDoc = userSnap.data();
 if (userDoc) {
  const index = getIndexOfVehicleInUser(vehicleId,userDoc);
  userDoc.vehicles.splice(index, 1);
  await admin.firestore().doc(`users/${user.userId}`).update({ vehicles: userDoc.vehicles });
 }
}
return null;
};

exports.deleteVehicleFromUsers = functions.firestore.document("/vehicles/{vehicleId}").onDelete((snap, context) => {
 const deletedVehicleId = context.params.vehicleId;
 const deletedVehicleUsers = snap.data().users;
 return deleteVehiclefromUsers(deletedVehicleUsers, deletedVehicleId);});

deleteVehiclesInOrg 函数应该触发,firebase 函数总是删除 orgs 文档中的所有车辆。这应该会触发 deleteVehicleFromUsers 函数,该函数会从用户文档中删除车辆。我的问题是有时会,有时不会。大多数时候,如果我有大约 10 辆车,它只会删除大约 6-8 辆车。但每次所有车辆都按应有的方式拆除。

当另一个函数 (deleteVehiclesInOrg) 删除了应该触发函数 deleteVehicleFromUsers 的文档时,是否有一个我没有正确处理的承诺,或者不可能依赖这样的后台触发函数?

【问题讨论】:

  • 嗨@Andreas,如果可能的话,请您在发生间歇性删除时提供日志。谢谢。
  • 下面的答案解决了这个问题,谢谢@MarcAnthonyB

标签: node.js firebase google-cloud-firestore google-cloud-functions


【解决方案1】:

欢迎来到 StackOverflow @andreas!

这是我的赌注:(只是猜测......)

deleteVehiclefromUsers 中的这一行:

await admin.firestore().doc(`users/${user.userId}`).update({ vehicles: userDoc.vehicles });

由不同的触发器同时执行,如果它们都使用同一个用户文档,它们将覆盖彼此的vehicles 数组。请记住,触发器是异步的,因此它们可以同时执行,而无需等待其他触发器首先完成。

示例:
vehicles = [A, B, C, D]

  • 触发器 1 读取用户并删除 C => vehicles = [A, B, D]
  • 触发器 2 读取用户并删除 D=> vehicles = [A, B, C]
  • 触发器 1 写入用户和存储 => vehicles = [A, B, D]
  • 触发器 2 写入用户和存储 => vehicles = [A, B, C]

最终的vehicles[A, B, C] 而不是[A, B]

证明确实如此:
在触发器的开头/结尾添加一些日志,以确保它们实际被触发,以及它们正在更新的用户文档 ID。 如果您要删除 10 辆车,而您的触发器没有(至少)触发 10 次,那么您的问题就出在其他地方。
(是的,一个非常偶然的触发器可能会触发不止一次)。

如何解决:
使用firestore transaction。这样,您将自动get() 用户文档和update() 它,这意味着您将把vehicles 数组写入您读取的同一个数组(而不是写入已经由另一个触发器写入的数组)。

应该是这样的:(未测试)

const userRef = admin.firestore().doc(`users/${user.userId}`);
await db.runTransaction(async (t) => {
  const userSnap = await t.get(userRef);
  if (userSnap.exists) {
    const userDoc = userSnap.data();
    const index = getIndexOfVehicleInUser(vehicleId,userDoc);
    userDoc.vehicles.splice(index, 1);
    t.update(userRef, { vehicles: userDoc.vehicles });
 }  
});

我个人建议仔细阅读有关交易的内容,这是一个需要牢记的重要概念。另请注意,如果发生冲突,事务可能会运行不止一次,并且如果在同一个文档上运行许多事务(例如一次删除属于同一用户的 100 辆汽车),则可能会永久失败。


额外:

针对您的评论,我所做的是估计大量可能要删除的文档,并在中间进行“休眠”批量删除。我知道这听起来很糟糕,但它确实有效,并且它给了足够的时间让触发器不会发生碰撞。

您只需要确保原始触发器(onDelete orgs)有足够的时间来完成,并且用户上的交易足够分散,不会发生太多冲突。

“数字”取决于您的用例。比如说:

  • 您估计一个庞大的组织将拥有 1000 辆汽车,假设您的计算基于 2000 辆汽车。
  • 函数的超时时间最多为 9 分钟。尝试在... 5 分钟内完成这一切。 您可以使用runWith() 来调整超时。例如: functions.firestore.runWith({ memory: '1GB', timeoutSeconds: 540 }).document("/orgs/{orgId}").onDelete(...);
  • 考虑最坏的情况:组织中的单个用户可以访问所有车辆。

您可以批量删除 20 辆汽车,中间间隔 1 秒: 1000 辆 / 20 辆批量 = 50 次迭代 x(1 秒睡眠 + 约 0.2 秒火库)=> 约 60 秒,非常粗略。

我会改变这个功能:(同样,根本没有测试)

const deleteVehiclesInOrg = async(deletedVehicles: string[]) => {
  const db = admin.firestore();
  const bulkWriter = db.bulkWriter();
  let i = 0;
  for (const vehicle of deletedVehicles) {
    const docRef = db.doc(vehicles/${vehicle});
    bulkWriter.delete( docRef );
    i++;
    if ( i % 20 == 0 ) { // bulk size
      bulkWriter.flush();
      await new Promise(r => setTimeout(r, 1000)); // sleep for a sec
    }
  }
  await bulkWriter.close(); // flush and wait the remaining are committed
  return null;
};

更好的是,要更多地分发它们,您可以使用 10 的块大小并休眠 500 毫秒。 (或批量 5 和睡眠 250 毫秒,你明白了......)

此外,您应该在users 中围绕您的事务添加一个try-catch,这样您至少可以记录错误,以防事务由于达到最大冲突而最终失败。

PS:注意bulkWriter 的使用比单独删除要高效得多。在上面的同一链接中找到此信息 (firestore transactions and bulk writes)。

【讨论】:

  • 非常感谢@maganap!你是绝对正确的,你的代码完美无缺!但是,如果我有 100 辆或更多车辆怎么办?这可能永远不会发生,但仍然存在。然后我是否应该有一个 setTimeout 函数来删除例如 50 辆车,等待 60 秒,删除 50 辆车,等待 60 秒,删除 50 辆车....直到所有车辆都被删除,以便给触发功能一些时间来更新用户并防止太多同一用户文档同时执行更新?还是有更好的解决方案?
  • 我会添加到答案中以便我可以扩展。给我几分钟@Andreas
  • @Andreas 那里!我发表评论只是为了触发通知您:-)
  • 再次感谢@maganap,非常感谢!我想我明白了。
猜你喜欢
  • 1970-01-01
  • 2018-07-21
  • 1970-01-01
  • 2020-08-31
  • 2020-05-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多