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) 中找到此信息。
我有一个如下所示的 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) 中找到此信息。