I have a Firestore Database that looks like this:
One collection for vehicles, one for organisations and one for users. In the organisations collection every document has a field called vehicles that contains the vehicles from the vehicle collection that this company owns. Every document in the users collection has a field named vehicles which contains all the vehicles that this user have access to. In my app I can delete the whole organisation (delete the document in the orgs collection). Then I have cloud functions that takes care of the rest. Or atleast should.
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;
};
This trigger function above deletes all the vehicles from this organisation which trigger this function below when a document from the vehicle collection is deleted:
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);});
The deleteVehiclesInOrg function trigger as it should, firebase function always deletes all the vehicles that was in the orgs document. This should trigger the deleteVehicleFromUsers function which deletes the vehicles from the user document. My problem is that sometimes it does and sometimes it doesn't. Most of the times if I have around 10 vehicles it only deletes about 6-8. But every time all the vehicles are being removed as they should.
Is there a promise that I don't handle correctly or isn't it possible to rely on background trigger functions like this, when it was another function (deleteVehiclesInOrg) that deleted the document that should trigger the function deleteVehicleFromUsers?
CodePudding user response:
Welcome to StackOverflow @andreas!
Here's my bet: (just guessing...)
This line in deleteVehiclefromUsers
:
await admin.firestore().doc(`users/${user.userId}`).update({ vehicles: userDoc.vehicles });
is being executed simultaneously by different triggers and, if they're all working with the same user document, they're overwriting each other's vehicles
array. Remember triggers are async so they may be executed concurrently without waiting for the other triggers to finish first.
Example:
vehicles = [A, B, C, D]
- Trigger 1 reads user and removes
C
=>vehicles = [A, B, D]
- Trigger 2 reads user and removes
D
=>vehicles = [A, B, C]
- Trigger 1 writes user and stores =>
vehicles = [A, B, D]
- Trigger 2 writes user and stores =>
vehicles = [A, B, C]
Final vehicles
is [A, B, C]
instead of [A, B]
.
Prove this is actually the case:
Add some logs to the beginning/end of your trigger, just to make sure they were actually triggered, and the user document id they're updating.
If you're deleting 10 vehicles and your trigger doesn't trigger (at least) 10 times, your problem is somewhere else.
(yes, a trigger very very occasionally may trigger more than once).
How to solve it:
Use a firestore transaction. This way, you will get()
the user document and update()
it atomically, meaning you'll be writing the vehicles
array to the same array you read (and not to the array that was already written by another trigger).
That would be something like this: (not tested)
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 });
}
});
I personally recommend reading thoughtfully about transactions, they're an important concept to have in mind. Also notice the transaction may run more than once in case of collisions and may permanently fail if many many transactions are being run on the same document (like deleting at once 100 vehicles that belong to the same user, for example).