Firebase concurrent read/write - javascript

I use Firebase transactions to get compare value and then update the value of a document in a collection.
but when I use the same data and send the query at the same time, they both read the same value so the check I do pass for both, and I have a bad result a the end.
the field is decremented twice.
my transaction is:
let docRef = db.collection('X').doc('SF');
let reduceValue = 20;
let transaction = db.runTransaction(t => {
return t.get(docRef)
.then(doc => {
let value = doc.data().y ;
if (value >= reduceValue) {
t.update(cptRef, { y:FieldValue.increment(-reduceValue) });
return Promise.resolve('Y increased');
} else {
return Promise.reject('sorry y is ');
}
});
}).then(result => {
console.log('Transaction success', result);
}).catch(err => {
console.log('Transaction failure:', err);
});
Thanks.

If I understand correctly your question, what you are describing is the correct behaviour of a Firestore Transaction executed with one of the Firestore Client SDKs:
You are calling twice a function that aims at decrementing a counter;
At the end, the counter is decremented twice.
The fact that your two calls are executed "at the same time" should not change the result: The counter should be decremented twice.
This is exactly what the Transaction ensures: In the case of a concurrent edit, Cloud Firestore runs the entire transaction again, ensuring that the initial doc (db.collection('X').doc('SF') in your case) has not changed during the Transaction. If it is the case, it will retry the operation with the new data.
This is because the Client SDKs use optimistic concurrency for Transactions and that, consequently, there is no locking of the documents.
I suggest you watch the following official video which explains that in detail.
You will also see in this video that for Transactions executed from a back-end (e.g. using the Admin SDK) the mechanism is not the same: they implement pessimistic concurrency (locking documents during transactions).

Related

Firestore transactions showing unexpected behaviour when used in cloud functions

I am writing an app that features an inventory in which users can reserve products. I want to ensure that 2 users cannot simultaneously reserve a product at the same time, for this, I intend on using transactions. When using transactions from the Firebase SDK, everything works as intended, but I am getting unexpected behavior when using transactions from a callable cloud function. To simulate the use case where 2 users happen to reserve the same product, I use setTimeout in my cloud function to halt the function for 3 seconds. I am launching this function from 2 different clients with different user contexts.
export const reserveProduct = functions.https.onCall(async (data,context) => {
function testTimeout(){
return new Promise((resolve,reject) => {
setTimeout(()=> {
return resolve(true)
},3000)
})
}
if(!context.auth){
return {
error: `You must be logged in to reserve products`
}
}else{
const productRef = admin.firestore().collection('products').doc(data.productID)
const userRef = admin.firestore().collection('users').doc(context.auth.uid)
return admin.firestore().runTransaction((transaction) => {
return transaction.get(productRef).then(async(doc) => {
if(doc.get('status') == 'reserved'){
throw "Document already reserved!"
}else{
console.log("Product not reserved, reserving now!")
}
await testTimeout()
transaction.update(productRef, {status: 'reserved'});
transaction.update(userRef, {reserved: admin.firestore.FieldValue.arrayUnion(data.productID)})
})
}).then(() => {
console.log("Transaction Successfully committed !")
}).catch((error) => {
throw "Transaction failed, product already reserved"
})
}
After running this function call from 2 different clients simultaneously, The function call from my first client returns successfully as expected, but only after roughly 35s (which is way too long for the simplicity of the transaction). However, the second function call times out without returning any value. I have not seen any documentation explicitly stating the use of transactions in callable cloud functions, nor should it be affected when used within the emulator.
I am expecting to simply get a return value for whichever function call is able to modify the data first, and catch the error from the function which has retried and validated the reserved state.
Any help would be appreciated, thanks!
One major difference between the two places is in the way the SDKs used handle transactions:
The client-side SDKs use an optimistic compare-and-set approach for transactions, meaning that they pass the values you read in the transaction with the data you're writing. The server then only writes the new data if the documents you read haven't been updated.
The server-side SDKs (used in your Cloud Function) use a more traditional pessimistic approach for transactions, and place a lock on each document that you read in the transaction.
You can read more about database contention in the SDKs in the documentation.
While I'm not exactly certain how this is affecting your code, I suspect it is relevant to the difference in behavior you're seeing between the client-side and server-side implementations.

What is the best way to handle two async calls that must both pass and make an "irreversible" change?

I am currently wondering about this issue, I have a team to which I want to add a user (so write a new user to the database for that team) and I also want to increase the amount of users that team needs to pay for (I use stripe subscriptions).
async handleNewUser(user, teamId){
await addUserToTeamInDatabase(user, teamId)
await incrementSubscriberQuantityInStripe(teamId)
}
The problem is, which one do I do first? I recently ran into an issue where users were being added but the subscriber count was not increasing. However, if I reverse them and increment first and then write to database and something goes wrong in this last part, the client pays more but does not get a new member added. One possible way of approaching this is with try catch:
handleNewUser(user, teamId) {
let userAddedToDatabase = false
let userAddedInStripe = false
try {
await addUserToTeamInDatabase(user, teamId)
userAddedToDatabase = true
await incrementSubscriberQuantityInStripe(teamId)
userAddedToStripe = true
} catch (error) {
if (userAddedToDatabase && !userAddedInStripe) {
await removeUserFromTeamInDatabase()
}
}
}
So I'm writing the new user to the database and then making a call to the stripe API.
Is there a better way to approach this because it feels clumsy. Also, is there a pattern to address this problem or a name for it?
I'm using Firebase realtime database.
Thanks everyone!
What you want to perform is a transaction. In databases a transaction is a group of operations that is successful if all of its operations are successful. If at least one operation fails, no changes are made (all the other operations are cancelled or rolled back).
And Realtime Database supports transactions! Check the documentation
If both operations would be in the same database you'd normally bundle them in a transaction and the DB will revert to initial state if one of them fails. In your case you have operations in different external systems (DB & Stripe) so you'll have to implement the transactional logic yourself.
You could simplify your example by checking where the error comes from in the catch clause. Then you can get rid of the flags. Something like this:
handleNewUser(user, teamId) {
try {
await addUserToTeamInDatabase(user, teamId)
await incrementSubscriberQuantityInStripe(teamId)
} catch (error) {
// If we fail to increment subscriber in Stripe,
// cancel transaction by removing user from DB
if (instanceof error StripeError) {
await removeUserFromTeamInDatabase(user, teamId)
}
// Re-throw error upstream
throw error;
}
}
I use instanceof but you you change the conditional logic to fit your program.

How to (using React JS web) and Firestore, can you find out when a chatRoom (on the Firestore Database) receives new messages?

I am trying to build an app using FireStore and React JS (Web)
My Firestore database basically has:
A collection of ChatRooms ChatRooms
Every chat-room has many messages which is a subcollection, for example:
this.db.collection("ChatRooms").doc(phone-number-here).collection("messages")
Also, every chat-room has some client info like first-name, last-name etc, and one that's very important:
lastVisited which is a timestamp (or firestamp whatever)
I figured I would put a React Hook that updates every second the lastVisited field, which means to try to record as accurately as possible on Firestore the last time I left a chat-room.
Based on that, I want to retrieve all the messages for every customer (chat-room) that came in after the last visit,
=> lastVisited field. :)
And show a notification.
I have tried from .onSnapshot listener on the messages subcollection, and a combination of Firestore Transactions but I haven't been lucky. My app is buggy and it keeps showing two, then one, then nothing, back to two, etc, and I am suffering much.
Here's my code!
Please I appreciate ANY help!!!
unread_messages = currentUser => {
const chatRoomsQuery = this.db.collection("ChatRooms");
// const messagesQuery = this.db.collection("ChatRooms");
return chatRoomsQuery.get().then(snapshot => {
return snapshot.forEach(chatRoom => {
const mess = chatRoomsQuery
.doc(chatRoom.id)
.collection("messages")
.where("from", "==", chatRoom.id)
.orderBy("firestamp", "desc")
.limit(5);
// the limit of the messages could change to 10 on production
return mess.onSnapshot(snapshot => {
console.log("snapshot SIZE: ", snapshot.size);
return snapshot.forEach(message => {
// console.log(message.data());
const chatRef = this.db
.collection("ChatRooms")
.doc(message.data().from);
// run transaction
return this.db
.runTransaction(transaction => {
return transaction.get(chatRef).then(doc => {
// console.log("currentUser: ", currentUser);
// console.log("doc: ", doc.data());
if (!doc.exists) return;
if (
currentUser !== null &&
message.data().from === currentUser.phone
) {
// the update it
transaction.update(chatRef, {
unread_messages: []
});
}
// else
else if (
new Date(message.data().timestamp).getTime() >
new Date(doc.data().lastVisited).getTime()
) {
console.log("THIS IS/ARE THE ONES:", message.data());
// newMessages.push(message.data().customer_response);
// the update it
transaction.update(chatRef, {
unread_messages: Array.from(
new Set([
...doc.data().unread_messages,
message.data().customer_response
])
)
});
}
});
})
.then(function() {
console.log("Transaction successfully committed!");
})
.catch(function(error) {
console.log("Transaction failed: ", error);
});
});
});
});
});
};
Searching about it, it seems that the best option for you to achieve that comparison, would be to convert your timestamps in milliseconds, using the method toMillis(). This way, you should be able to compare the results better and easier - more information on the method can be found in the official documentation here - of the timestamps of last message and last access.
I believe this would be your best option as it's mentioned in this Community post here, that this would be the only solution for comparing timestamps on Firestore - there is a method called isEqual(), but it doesn't make sense for your use case.
I would recommend you to give it a try using this to compare the timestamps for your application. Besides that, there is another question from the Community - accessible here: How to compare firebase timestamps? - where the user has a similar use cases and purpose as yours, that I believe might help you with some ideas and thoughts as well.
Let me know if the information helped you!

firestore query snapshot reads more than once

I have a server-side nodejs program which monitors/"listens" to a particular firestore location.
db.collection('temp').where('processed', '==', 'false').onSnapshot(snapshots =>
{
snapshots.forEach(snapshot =>
{
//processes some logic
//db.collection('temp').document(snapshot.id).update('processed': true);
}
}
There are no issues if the query snapshot returns only one snapshot, but if the query snapshot returns more than one snapshot, the logic will be called more times than its intended. For example, if concurrently write n times to the monitored location, the logic is called n^2 times. This will incur costs towards read quotas. How can I make sure that it's only being read once per document?
By looping over snapshot in your onSnapshot() callback you are handling all documents that match the query each time that something changes.
If you only want to handle the documents that were changed, you'll want to loop over snapshot.documentChanges as shown in the Firebase documentation.
If you're always writing documents with processed = false, then you can just handle change.type = added:
db.collection('temp').where('processed', '==', 'false').onSnapshot(snapshots =>
snapshots.docChanges.forEach(function(change) {
if (change.type === "added") {
//processes some logic
//db.collection('temp').document(change.id).update('processed': true);
}

Self-triggered perpetually running Firebase process using NodeJS

I have a set of records that I would like to update sequentially in perpetuity. Basically:
Get least recently updated record
Update record
Set date of record to now (aka. send it to the back of the list)
Back to step 1
Here is what I was thinking using Firebase:
// update record function
var updateRecord = function() {
// get least recently updated record
firebaseOOO.limit(1).once('value', function(snapshot) {
key = _.keys(snapshot.val())[0];
/*
* do 1-5 seconds of non-Firebase processing here
*/
snapshot.ref().child(key).transaction(
// update record
function(data) {
return updatedData;
},
// update priority after commit (would like to do it in transaction)
function(error, committed, snap2) {
snap2.ref().setPriority(snap2.dateUpdated);
}
);
});
};
// listen whenever priority changes (aka. new item needs processing)
firebaseOOO.on('child_moved', function(snapshot) {
updateRecord();
});
// kick off the whole thing
updateRecord();
Is this a reasonable thing to do?
In general, this type of daemon is precisely what was envisioned for use with the Firebase NodeJS client. So, the approach looks good.
However, in the on() call it looks like you're dropping the snapshot that's being passed in on the floor. This might be application specific to what you're doing, but it would be more efficient to consume that snapshot in relation to the once() that happens in the updateRecord().

Categories

Resources