Problem:
I’m trying to figure out if Firebase Realtime databases have an “All or nothing” Transaction similar to the one from Firestore – Transaction.
I need to perform this sort of transaction on a Cloud function.
To add more context to this, I’m trying to listen to the creation of new children to a node. Whenever a new child node gets added, I want to perform a bunch of actions (reads, writes, updates, and deletes), and I want to ensure that either all of them go through or none of them do. Also, I want to ensure that the data changes to any of the above-mentioned nodes during this time don’t affect this transaction.
Here’s what I’m doing:
export const matchPlayers = functions.database
.ref(`match_queue/{newEntryId}`)
.onCreate((createdChild) => {
const queue = db.ref('match_queue');
queue.get().then((snapshot) => {
snapshot.forEach((child) => {
if (child.key != createdChild.key && !child.val().matched) {
matchPlayers(child, createdChild);
return;
}
});
});
return null;
});
function matchPlayers(
player1Snapshot: DataSnapshot,
player2Snapshot: DataSnapshot,
) {
const player1 = player1Snapshot.val();
const player2 = player2Snapshot.val();
const match = {
player_1_id: player1.id,
player_2_id: player2.id,
};
const matchId = db.ref("matches").push(match).key;
const promises = [];
promises.push(db.ref("users")
.child(player1.id).update({
current_match_id: matchId,
}));
promises.push(db.ref("users")
.child(player2.id).update({
current_match_id: matchId,
}));
promises.push(db
.ref(`${MATCH_QUEUE_REF}/` + player1Snapshot.key).remove());
promises.push(db
.ref(`${MATCH_QUEUE_REF}/` + player2Snapshot.key).remove());
return Promise.all(promises);
}
This above function listens for the creation of new children nodes to “match_queue”.
If there is a new child added, I want to perform the following actions:
- Read match_queue’s children and perform some logic (basically match the created node with a different child node from “match_queue” – simple matchmaking).
- Add a new child node to “matches” (a different parent node).
- Update the individual “users” that got matched.
- And finally, delete the “createdChild” node from above.
I’m able to perform the DB updates mentioned here, but I’m not able to figure out how to ensure that while this transaction is going on and new data get’s written to the db, these two children that are currently getting matched, don’t get picked up again.
So say the children to match_queue are “abc” and “def”. When “def” gets created, the cloud function runs and tries to match it to “abc”. But during this transaction, another child “ghi” gets created. I want to ensure that “ghi” isn’t getting matched to either “abc” or “def” since they’re in a transaction that’s in progress.
Solution:
Firebase has two mechanisms for performing transactional write operations:
- Transactions that read from a single path in the database and then write a new value to that path.
- Multi-path writes that write to multiple paths in the database atomically, but can’t read any data.
From a quick scan it looks like you need to update multiple, disjunct paths in the database, so a multi-path write seems the way to go. Something like:
const matchId = db.ref("matches").push(match).key;
const updates = {};
updates[`users/$player1.id}/current_match_id`] = matchId;
updates[`users/${player2.id}/current_match_id`] = matchId;
updates[`${MATCH_QUEUE_REF}/${player1Snapshot.key}`] = null; // writing null removes the node
updates[`${MATCH_QUEUE_REF}/${player2Snapshot.key}`] = null;
return db.ref().update(updates);
Advanced update
If you want to prevent another user from modifying a value while you perform a multi-path update, you’re essentially looking for a multi-path transaction which Firebase Realtime Database doesn’t natively support. You can roll your own multi-path transaction mechanism, but that’s definitely quite involved.
I typically include some version number in the data I read, that I then write back/increment in the multi-path updates, and check in security rules that the version is what I expect. In the client-codeyouI then catch write rules failure and retry. That’s pretty much what transactions do automatically, just with a lot more work (and knowledge about your use case) on your own part.
Also see:
- How can I use a transaction while performing a multi-location update in Firebase?
- Is the way the Firebase database quickstart handles counts secure?
- Firebase – One transaction for 2 paths in real time database
- how to write batched transactions for firebase admin realtime db?
- How can I combine updateChildren and runTransaction?