Firebase 函数触发 onDelete 有时会起作用

Firebase functions trigger onDelete works sometimes

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

vehicle collection

orgs collection

users collection

一个集合用于车辆,一个集合用于组织,一个集合用于用户。在组织集合中,每个文档都有一个名为 vehicles 的字段,其中包含该公司拥有的车辆集合中的车辆。 users 集合中的每个文档都有一个名为 vehicles 的字段,其中包含该用户有权访问的所有车辆。在我的应用程序中,我可以删除整个组织(删除 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 的文档时,是否存在我未正确处理或无法依赖此类后台触发函数的承诺?

欢迎来到 Whosebug @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]

证明确实如此:
将一些日志添加到触发器的 beginning/end 中,只是为了确保它们确实被触发,以及它们正在更新的用户文档 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 秒 firestore)=> ~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 毫秒。 (或者 bulk 5 和 sleep 250 ms,你明白了......)

此外,您应该在 users 中为您的交易设置一个 try-catch,这样您至少可以记录错误日志,以防交易因达到最大冲突而最终失败。

PS:注意 bulkWriter 的使用,它比单独删除更有效。在上面相同的 link (firestore transactions and bulk writes) 中找到此信息。