Home > Software engineering >  firebase function, counter takes too long to update
firebase function, counter takes too long to update

Time:09-20

I have implemented a counter variable in my real time database, the users are able to call the function and upon 4 calls it should run some code. The problem is that it may take too long to run and update the counter, so if the counter = 1 and two people call the function at a relative same time both of them will get

datasnapshot.child("counter").val() = 1 

and both of them will set

promises.push(admin.database().ref('game/'   matchkey   '/counter').set((counter   1))) = 2

while it should be 3

how can I fix this ? (I guess by either making the update faster or forcing the function to wait for the previous one to end (not sure if possible), or something else that I am missing I am new to firebase & ts)

this is my code:

exports.functionTST = functions.https.onCall(async (data, context) => {
  const promises = [];
  JSON.parse(JSON.stringify(data), (key, value) => {
     parse values 
  });

  //set values
  promises.push(admin.database().ref('game/'   matchkey   '/'...).update({
      val1: val2,
    }))

    //counter
    admin.database().ref('game/'   matchkey).once('value')
      .then(datasnapshot => {
        let counter = datasnapshot.child("counter").val()
        if (counter === null) {
          promises.push(admin.database().ref('game/'   matchkey   '/counter').set(1))
        } else if (counter < 3) {
          promises.push(admin.database().ref('game/'   matchkey   '/counter').set((counter   1)))
        } else if (counter == 3) {
          promises.push(admin.database().ref('game/'   matchkey   '/counter').remove())
          //do other stuff
         }
     });

    Promise.all(promises);
    return null
  }
});

Thanks !!

CodePudding user response:

First, let's fix your indentation.

exports.functionTST = functions.https.onCall(async (data, context) => {
  const promises = [];
  JSON.parse(JSON.stringify(data), (key, value) => {
     // parse values 
  });

  // set values
  promises.push(
    admin.database()
      .ref('game/'   matchkey   '/'...)
      .update({
        val1: val2,
      })
  );

  // counter
  admin.database()
    .ref('game/'   matchkey)
    .once('value')
    .then(datasnapshot => {
      let counter = datasnapshot.child("counter").val()
      if (counter === null) {
        promises.push(admin.database().ref('game/'   matchkey   '/counter').set(1))
      } else if (counter < 3) {
        promises.push(admin.database().ref('game/'   matchkey   '/counter').set((counter   1)))
      } else if (counter == 3) {
        promises.push(admin.database().ref('game/'   matchkey   '/counter').remove())
        // do other stuff
      }
    });

  Promise.all(promises);
  return null
});

From this, we can now see a few issues.

The most notable error, is that you aren't awaiting or returning the Promise chain properly. When you don't wait for the promises to resolve, your function will get severely throttled and network requests (such as updating the database) can be skipped entirely.

Promises.all(promises)
return null; // return no data

should be

await Promises.all(promises) // <-- note addition of await here
return null; // return no data

or

return Promises.all(promises) // <-- note moving return here
  .then(() => null) // return no data

This next block of code does nothing useful. Were you meant to store the result of JSON.parse into a new variable? Depending on what you are trying to achieve Object.entries(data) might be a better choice.

JSON.parse(JSON.stringify(data), (key, value) => {
  // parse values 
});

This next block of code spawns a floating Promise - you aren't returning it, nor are you storing it into an array like promises - so it will cause inconsistent behaviour (like never updating the counter).

admin.database()
  .ref('game/'   matchkey)
  .once('value')
  .then(/* ... */)

The entirety of the code shown in this block should be replaced by a transaction operation instead to streamline the database updates and ensure that it is properly updated.

// set values
promises.push(
  admin.database()
    .ref('game/'   matchkey   '/'...)
    .update({
      val1: val2,
    })
);

// counter
admin.database()
  .ref('game/'   matchkey)
  .once('value')
  .then(datasnapshot => {
    let counter = datasnapshot.child("counter").val()
    if (counter === null) {
      promises.push(admin.database().ref('game/'   matchkey   '/counter').set(1))
    } else if (counter < 3) {
      promises.push(admin.database().ref('game/'   matchkey   '/counter').set((counter   1)))
    } else if (counter == 3) {
      promises.push(admin.database().ref('game/'   matchkey   '/counter').remove())
      //do other stuff
    }
  });

Promises.all(promises)
return null;

Adapting the transaction example and merging your other logic gives:

//counter
await admin.database() // <-- note addition of await here
  .ref('game/'   matchkey)
  .transaction((matchInfo) => {
    if (matchInfo) {
      // set updated values
      matchInfo[/* The '...' from .ref('game/'   matchkey   '/'...) */].val1 = val2;
      // e.g.
      // matchInfo.round1.val1 = val2;
      // OR
      // matchInfo.round1 = Object.assign(
      //   matchInfo.round1 || {}, // <-- creates the property when it is missing
      //   {
      //     val1: val2
      //   }
      // );

      // handle & update counter
      const counter = matchInfo.counter || 0;
      if (counter < 3) {
        matchInfo.counter = counter   1;
      } else {
        // clear counter and do other stuff
        delete matchInfo.counter;
        // e.g.
        // matchInfo.winner = matchInfo.players[0]
      }
    }
    return matchInfo; // apply changes
  });

return null; // return no data to the caller

After implementing the above block, your function can become something similar to:

exports.functionTST = functions.https.onCall(async (data, context) => {
  // parse request body
  const matchkey = data.matchkey;
  /* ... */

  // update match information
  await admin.database() // <-- note addition of await here
    .ref('game/'   matchkey)
    .transaction((matchInfo) => {
      /* ... */
    })
  
  return null;

To ensure that the function returns appropriate errors, you should surround each part in try-catch blocks and throw an appropriate HttpsError so failures can be handled gracefully by the caller on the client side:

exports.functionTST = functions.https.onCall(async (data, context) => {
  // parse request body
  try {
    const matchkey = data.matchkey;
    /* ... */
  } catch (err) {
    console.log('Unexpected error while handling request data: ', err);
    throw new functions.https.HttpsError('invalid-argument', 'Could not parse request data');
  }

  // update match information
  try {
    await admin.database() // <-- note addition of await here
      .ref('game/'   matchkey)
      .transaction((matchInfo) => {
        /* ... */
      })
  } catch (err) {
    console.log('Unexpected error while updating match information: ', err);
    throw new functions.https.HttpsError('internal', 'Could not update match information');
  }
  
  return null; // return no data to the caller
}

CodePudding user response:

First run after deploy take time because the cold start happening, And consider using transaction to prevent race condition.

  • Related