3

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

Bob Snyder
  • 37,759
  • 6
  • 111
  • 158
Sharan Mohandas
  • 861
  • 1
  • 9
  • 25
  • 2
    `But my Likes count are being updated incorrectly at certain times.` - can you elaborate on when those certain times occur, and under what circumstances? – Pat Needham Feb 13 '18 at 05:25
  • its irregular and occurs randomly. Should i return transaction.update(transRef, { likesCount: newLikesCount }); ? – Sharan Mohandas Feb 13 '18 at 05:27
  • Are you using a Firestore write event to trigger the Cloud Function? While Firestore is in beta, it's trigger behavior is subject to some limitations. See this answer: https://stackoverflow.com/q/48735862/4815718 – Bob Snyder Feb 13 '18 at 05:58
  • @BobSnyder. That's a show stopper. We can't just have idempotent functions. Right? Any knowledge on when these services would be stable – Sharan Mohandas Feb 13 '18 at 07:20
  • I have no knowledge of when (and if) the limitations will be removed. You could ask [Firebase Support](https://firebase.google.com/support/contact/troubleshooting/) – Bob Snyder Feb 13 '18 at 14:20

3 Answers3

6

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.

Sharan Mohandas
  • 861
  • 1
  • 9
  • 25
  • "as it is advertised" Do you have a reference to this? Firestore is not billed as an upgrade to RTDB. If you've seen this in any official Google docs I'd like to know. See comparisons [here](https://firebase.google.com/docs/firestore/rtdb-vs-firestore) and [here](https://firebase.googleblog.com/2017/10/cloud-firestore-for-rtdb-developers.html). – Kato Feb 14 '18 at 16:30
  • 4
    Cloud Firestore is Firebase's new flagship database for mobile app development. It improves on the successes of the Realtime Database with a new, more intuitive data model. Cloud Firestore also features richer, faster queries and scales better than the Realtime Database. https://firebase.google.com/docs/firestore/rtdb-vs-firestore. How does it scale when it can't even handles writes similar to RTDB – Sharan Mohandas Feb 15 '18 at 04:27
  • 1
    And that same article goes on to explain the advantages of each. Fail to see the "is an upgrade" anywhere there, sorry. It certainly tries to improve on RTDBs limitations as any subsequent product should, focused much more heavily on redundancy and reliability, trading off some microseconds of speed and a bit of the simplicity. All reasonable comparisons. – Kato Feb 16 '18 at 22:06
  • @SharanMohandas So do you mean the document update limitation is not applicable to Realtime database? What is the allowed max write rate for RTDB? – Ayyappa Jun 03 '20 at 16:59
1

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 });
        });
    });
Pat Needham
  • 5,698
  • 7
  • 43
  • 63
  • As this is an async cloud function I'm supposed to return a Promise to mark the completion. See below https://firebase.google.com/docs/functions/terminate-functions Use these recommended approaches to manage the lifecycle of your functions: Resolve functions that perform asynchronous processing (also known as "background functions") by returning a JavaScript promise. – Sharan Mohandas Feb 13 '18 at 05:41
  • Yes, you're right I missed that part. So in that case maybe you just need to get rid of one of the the initial two `return` statements you had (without adding another before the `transaction.update` – Pat Needham Feb 13 '18 at 05:48
  • My concern is that the irregularity of when your function is triggered makes it difficult to determine the root cause. Do you know if it's only when a transaction has many likes/dislikes within a short period of time, for example 5 dislikes and 8 likes within 2 seconds? I would try creating some test cases to see if you can reproduce that incorrect update count on a consistent basis. – Pat Needham Feb 13 '18 at 05:54
  • That's what I think as well. Need to know how the function performs concurrently – Sharan Mohandas Feb 13 '18 at 07:13
1

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.

Jason Berryman
  • 4,760
  • 1
  • 26
  • 42