Sharan Mohandas
Sharan Mohandas

Reputation: 871

Firestore transactions getting triggered multiple times resulting in wrong data

So I have a cloud function that is triggered each time a transaction is liked/unliked. This function increments/decrements the likesCount. I've used firestore transactions to achieve the same. I think the problem is the Code inside the Transaction block is getting executed multiple times, which may be correct as per the documentation.

But my Likes count are being updated incorrectly at certain times.

 return firestore.runTransaction(function (transaction) {
        return transaction.get(transRef).then(function (transDoc) {
            let currentLikesCount = transDoc.get("likesCount");
            if (event.data && !event.data.previous) {
                newLikesCount = currentLikesCount == 0 || isNaN(currentLikesCount) ? 1 : transDoc.get("likesCount") + 1;
            } else {
                newLikesCount = currentLikesCount == 0 || isNaN(currentLikesCount) ? 0 : transDoc.get("likesCount") - 1;
            }
            transaction.update(transRef, { likesCount: newLikesCount });
        });
    });

Anyone had similar experience

Upvotes: 4

Views: 3611

Answers (3)

Sharan Mohandas
Sharan Mohandas

Reputation: 871

Guys finally found out the cause for this unexpected behaviour.

Firestore isn't suitable for maintaining counters if your application is going to be traffic intensive. They have mentioned it in their documentation. The solution they suggest is to use a Distributed counter.

Many realtime apps have documents that act as counters. For example, you might count 'likes' on a post, or 'favorites' of a specific item.

In Cloud Firestore, you can only update a single document about once per second, which might be too low for some high-traffic applications.

https://cloud.google.com/firestore/docs/solutions/counters

I wasn't convinced with that approach as it's too complex for a simple use case, which is when I stumbled across the following blog

https://medium.com/evenbit/on-collision-course-with-cloud-firestore-7af26242bc2d

These guys used a combination of Firestore + Firebase thereby eliminating their weaknesses.

Cloud Firestore is sitting conveniently close to the Firebase Realtime Database, and the two are easily available to use, mix and match within an application. You can freely choose to store data in both places for your project, if that serves your needs.

So, why not use the Realtime database for one of its strengths: to manage fast data streams from distributed clients. Which is the one problem that arises when trying to aggregate and count data in the Firestore.

Its not correct to say that Firestore is an upgrade to the Realtime database (as it is advertised) but a different database with different purposes and both can and should coexist in a large scale application. That's my thought.

Upvotes: 6

Jason Berryman
Jason Berryman

Reputation: 4898

Timeouts

First of all, check your Cloud Functions logs to see if you get any timeout messages.

Function execution took 60087 ms, finished with status: 'timeout'

If so, sort out your function so that it returns a Promise.resolve(). And shows

Function execution took 344 ms, finished with status: 'ok'

Idempotency

Secondly, write your data so that the function is idempotent. When your function runs, write a value to the document that you are reading. You can then check if that value exists before running the function again.

See this example for ensuring that functions are only run once.

Upvotes: 1

Pat Needham
Pat Needham

Reputation: 5918

It might have something to do with what you're returning from the function, as you have

return transaction.get(transRef).then(function (transDoc) { ... })

And then another return inside that callback, but no return inside the inner-most nested callback. So it might not be executing the transaction.update. Try removing the first two return keywords and add one before transaction.update:

firestore.runTransaction(function (transaction) {
        transaction.get(transRef).then(function (transDoc) {
            let currentLikesCount = transDoc.get("likesCount");
            if (event.data && !event.data.previous) {
                newLikesCount = currentLikesCount == 0 || isNaN(currentLikesCount) ? 1 : transDoc.get("likesCount") + 1;
            } else {
                newLikesCount = currentLikesCount == 0 || isNaN(currentLikesCount) ? 0 : transDoc.get("likesCount") - 1;
            }
            return transaction.update(transRef, { likesCount: newLikesCount });
        });
    });

Upvotes: 1

Related Questions