0
votes

Shortly, imagine I have a Cloud Firestore DB where I store some users data such as email, geo-location data (as geopoint) and some other things. In Cloud Functions I have "myFunc" that runs trying to "link" two users between them based on a geo-query (I use GeoFirestore for it).

Now everything works well, but I cannot figure out how to avoid this kind of situation:

  • User A calls myFunc trying to find a person to be associated with, and finds User B as a possible one.
  • At the same time, User B calls myFunc too, trying to find a person to be associated with, BUT finds User C as possible one.

In this case User A would be associated with User B, but User B would be associated with User C.

I already have a field called "associated" set to FALSE on each user initialization, that becomes TRUE whenever a new possible association has been found.

But this code cannot guarantee the right association if User A and User B trigger the function at the same time, because at the moment in which the function triggered by User A will find User B, the "associated" field of B will be still set to false because B is still searching and has not found anybody yet.

I need to find a solution otherwise I'll end up having wrong associations ( User A pointing at User B, but User B pointing at User C ).

I also thought about adding a snapshotListener to the user who is searching, so in that way if another User would update the searching user's document, I could terminate the function, but I'm not really sure it will work as expected.

I'd be incredibly grateful if you could help me with this problem. Thanks a lot!

Cheers, David

HERE IS MY CODE:

exports.myFunction = functions.region('europe-west1').https.onCall( async (data , context) => {
    
      const userDoc = await firestore.collection('myCollection').doc(context.auth.token.email).get();
    
      if (!userDoc.exists) {
        return null;
      }
    
      const userData = userDoc.data();

      if (userData.associated) { // IF THE USER HAS ALREADY BEEN ASSOCIATED
        return null;
      }
    
      const latitude = userData.g.geopoint["latitude"];
      const longitude = userData.g.geopoint["longitude"];
    
      // Create a GeoQuery based on a location
      const query = geocollection.near({ center: new firebase.firestore.GeoPoint(latitude, longitude), radius: userData.maxDistance });
    
      // Get query (as Promise)

        let otherUser = []; // ARRAY TO SAVE THE FIRST USER FOUND

        query.get().then((value) => {

      // CHECK EVERY USER DOC
        value.docs.map((doc) => {
    
          doc['data'] = doc['data']();
    
      // IF THE USER HAS NOT BEEN ASSOCIATED YET
            if (!doc['data'].associated) { 

      // SAVE ONLY THE FIRST USER FOUND
               if (otherUser.length < 1) {
                   otherUser = doc['data'];
               }
            }
          return null;
        });
    
        return value.docs;

      }).catch(error => console.log("ERROR FOUND: ", error));
    
     // HERE I HAVE TO RETURN AN .update() OF DATA ON 2 DOCUMENTS, IN ORDER TO UPDATE THE "associated" and the "userAssociated" FIELDS OF THE USER WHO WAS SEARCHING AND THE USER FOUND 
      return ........update({
             associated: true,
             userAssociated: otherUser.name
      });    
    }); // END FUNCTION
1
From what you're describing it sounds like your need a transaction and/or security rules. It's hard to be more specific, as I don't fully understand where you got stuck while implementing this. Is it possible for you to share the minimal, complete/standalone code that reproduces the problem?Frank van Puffelen
Hi @FrankvanPuffelen and thanks for your answer! I've just added my code to my question, in the meantime I'm reading some documentation about transaction in order to see if they can fit my needs.Davide Dell'Aira

1 Answers

1
votes

You should use a Transaction in your Cloud Function. Since Cloud Functions are using the Admin SDK in the back-end, Transactions in a Cloud Function use pessimistic concurrency controls.

Pessimistic transactions use database locks to prevent other operations from modifying data.

See the doc form more details. In particular, you will read that:

In the server client libraries, transactions place locks on the documents they read. A transaction's lock on a document blocks other transactions, batched writes, and non-transactional writes from changing that document. A transaction releases its document locks at commit time. It also releases its locks if it times out or fails for any reason.

When a transaction locks a document, other write operations must wait for the transaction to release its lock. Transactions acquire their locks in chronological order.