0

I am making a 1 vs 1 game matching system with the help of realtime database. When user sign in, it will generate a record in users table. When there are 2 player user status are placeholder then cloud function will generate a gameInfo for these 2 player and change their user status to waiting. What I am doing is make a trigger function to observe users/{uid} when it status child are placehold then call matchmaker function to generate gameInfo with unique gameId. But the problem is when trigger onWrite function run first time, theoretically there are only one user with placeholder status, however the database changed so quick in some case when iterate the database users table it has two player with placeholder status. So there are wired things happen sometimes, after two trigger function callbacks there should be only one gameId record in gameInfo because two player join in one game room with unique gameId, but it generate two unique gameId record sometimes. How can I avoid this wired thing happen. Here is the database table. database table

And here is part of my cloud function code. For the matchmaker function, it will generate gameId record in games table if there are two players in the map waitingPlayers.

export const searchWaitingPlayers = functions.database.ref("users/{uid}").onWrite( (change, context) => {
  if (!change.after.exists()) {
    console.log("uid deleted");
    return null;
  } else {
    if (change.after.child("status").val() !== "placeholder") {
      console.log("user's status is not placeholder");
      waitingPlayers.delete(change.after.key);
    } else {
      waitingPlayers.clear();
      admin.database().ref("users").once("value").then(async (users) => {
        users.forEach((user) => {
          if (user.child("status").val() === "placeholder") {
            waitingPlayers.set(user.key, user.child("username").val());
          }
        });
        matchmaker();
      });
    }
    return null;
  }
});
Dong Wang
  • 334
  • 1
  • 10

2 Answers2

0

I'm not exactly sure when your problem occurs, but it seems some sort of race condition between two updates happening to the data.

To prevent race condition for data updates, you'll want to use a transaction.

Frank van Puffelen
  • 565,676
  • 79
  • 828
  • 807
  • Can transaction ensure onWrite callback call only its own data change rather than the next data change? – Dong Wang Nov 07 '21 at 00:33
  • A transaction can only prevent concurrent access in the write operation it covers. So once the `onWrite` Cloud Function triggers, there is no way to prevent that write anymore. The best you can do at that point is roll back the operation (by looking at `change.before`), but that is likely fraught with problems. If you need cross-client transactionality, the transactions should happen on the clients. – Frank van Puffelen Nov 07 '21 at 00:41
  • Thanks for your reply. I am wondering when onWrite Cloud Function triggers whether it will prevent other write operations from client? Can transaction make it? I hope transaction implemention in client the onWrite triggers should prevent write operations from client after that time. – Dong Wang Nov 07 '21 at 01:06
  • Once the `onWrite` Cloud Function triggers, there is no way to prevent that write anymore - nor a way to influence other triggers. If you need cross-client transactionality, the transactions should happen on the clients. – Frank van Puffelen Nov 07 '21 at 01:29
0

There is no way to disable a Cloud Function programmatically without just deleting it. However, a solution is to use feature flags to determine if a Cloud Function performs its main function. The following code can be referred to for this.

exports.onUpload = functions.database 
.ref("/uploads/{uploadId}") 
.onWrite((event) => { 
return ifEnabled("transcribe").then(() => {
console.log("transcription is enabled: calling Cloud Speech");
            ... 
 })
});

The ifEnabled is a simple helper function that checks (also in Realtime Database) if the feature is enabled:

function ifEnabled(feature) { 
console.log("Checking if feature '"+feature+"' is enabled"); 
return new Promise((resolve, reject) => { 
   admin.database().ref("/config/features")
     .child(feature) 
                  .once('value') 
     .then(snapshot => { 
        if (snapshot.val()) { 
        resolve(snapshot.val()); 
    } else
      { 
      reject("No value or 'falsy' value found"); 
      }
   });
 });
}
Mousumi Roy
  • 609
  • 1
  • 6