Sahand
Sahand

Reputation: 8370

Array stored in firebase server sometimes has its elements set to undefined

I'm getting a weird error. First let me explain my database structure. I'm storing bookable beds for different hotels in my database. The structure is like this:

/beds
|
|- hotel1
|    |---- bed1
|    |
|    |---- bed2
|
|- hotel2
|    |---- bed1
|    |
|    |---- bed2
|
|- hotel3
     etc...

A user has the ability to pre-book a bed so that no one else can book it while he's considering to book it or not. There is a timer of 5 minutes for this. To avoid having too many timers, I have a list in my server which holds a list for each hotel which in turn holds an object for each bed of the hotel:

const hotelBedTimeouts = [];
var beds = db.ref('/beds');

// Initialise the bed timeout holder object
beds.once("value", function(snapshot){
  var hotels = snapshot.val();

  for (var i = 0; i < hotels.length; i++) {
    // push empty list to be filled with lists holding individual bed timeouts
    if(hotels[i]){
      hotelBedTimeouts.push([]);
      for(var j = 0; j < hotels[i].length; j++) {
        // this list will hold all timeouts for this bed
        hotelBedTimeouts[i].push({});
      }
    } else {
      hotelBedTimeouts.push(undefined);
    }
  }
});

That's how I create the empty timer-holder structure. Then I set a timer whenever a bed is pre-booked by a customer with a firebase function. I also use the function to cancel the timer when the user exits the page:

// Frees a bed after a set amount of time
exports.scheduleFreeBed = functions.database.ref('/beds/{hotelIndex}/{bedIndex}/email').onUpdate( (snapshot, context) => {
  var originalEmail = snapshot.after.val();
  var hotelIndex = context.params.hotelIndex;
  var bedIndex = context.params.bedIndex;
  if (originalEmail === -1) {

    console.log("Cancelling timeout for chair number " + bedIndex + " with...");
    console.log("hotelIndex: " + hotelIndex);
    console.log("hotelBedTimeouts[hotelIndex]:");
    console.log(hotelBedTimeouts[hotelIndex]);
    console.log("hotelBedTimeouts[hotelIndex][bedIndex]");
    console.log(hotelBedTimeouts[hotelIndex][bedIndex]);

    clearTimeout(hotelBedTimeouts[hotelIndex][bedIndex].timeoutFunc); // clear current timeoutfunc
    return 0; // Do nothing
  }

  console.log("Setting timeout for bed number " + bedIndex + " with...");
  console.log("hotelIndex: " + hotelIndex);
  console.log("hotelBedTimeouts[hotelIndex]:");
  console.log(hotelBedTimeouts[hotelIndex]);
  console.log("hotelBedTimeouts[hotelIndex][bedIndex]");
  console.log(hotelBedTimeouts[hotelIndex][bedIndex]);

  // replace old timeout function
  hotelBedTimeouts[hotelIndex][bedIndex].timeoutFunc = setTimeout(function () {
    var bedRef = admin.database().ref(`/beds/${hotelIndex}/${bedIndex}`);
    bedRef.once("value", function(bedSnap){
      var bed = bedSnap.val();
      var booked = bed.booked;
      if (!booked) {
        var currentEmail = bed.email;
        // Check if current bed/email is the same as originalEmail
        if (currentEmail === originalEmail) {
          bedSnap.child("email").ref.set(-1, function() {
            console.log("Freed bed");
          });
        }
      }
    });
  }, 300000); // 5 min timeout

  return 0;
});

This works fine most of the time. However, if I pre-book many beds at the same time, there tends to be errors for some of the chairs. Here's how an error looks:

Cancelling timeout for bed number 24 with...    

hotelIndex: 1

hotelBedTimeouts[hotelIndex]:

undefined

hotelBedTimeouts[hotelIndex][bedIndex]

TypeError: Cannot read property '24' of undefined
    at exports.scheduleFreeBed.functions.database.ref.onUpdate (/user_code/index.js:698:50)
    at Object.<anonymous> (/user_code/node_modules/firebase-functions/lib/cloud-functions.js:112:27)
    at next (native)
    at /user_code/node_modules/firebase-functions/lib/cloud-functions.js:28:71
    at __awaiter (/user_code/node_modules/firebase-functions/lib/cloud-functions.js:24:12)
    at cloudFunction (/user_code/node_modules/firebase-functions/lib/cloud-functions.js:82:36)
    at /var/tmp/worker/worker.js:728:24
    at process._tickDomainCallback (internal/process/next_tick.js:135:7)

It looks like hotelBedTimeouts[24] is undefined. This is inexplicable to me for two reasons:

  1. I've already populated hotelBedTimeouts with a list for each hotel holding empty objects for beds 1 - 30. hotelBedTimeouts[24] evaluating to undefined should thus be impossible.
  2. The same bed works fine to pre-book and "un"-pre-book by itself right after the error.

What is the reason for this error and how do I fix it?

Upvotes: 1

Views: 58

Answers (1)

FatalMerlin
FatalMerlin

Reputation: 1540

Firebase is highly async

This means that if your code depends on a certain execution order, you need to make sure that it executes in that order.

The once function returns a Promise (more information on Promises here). You could register the scheduleFreeBed function within the Promise.then() callback function, so the onUpdate gets registered after the initialization has completed.

For your example:

// Initialise the bed timeout holder object
beds.once("value", function (snapshot) {
    // your existing code...
}).then(() => {
    // Frees a bed after a set amount of time
    exports.scheduleFreeBed = functions.database.ref('/beds/{hotelIndex}/{bedIndex}/email').onUpdate( (snapshot, context) => {
        // your existing code...
    });
})

This will make sure that scheduleFreeBed can only be triggered after your initialization has finished.

This also means that the onUpdate will be ignored if the data is changed during the initialization!

Since the above apparently doesn't work because obviously asynchronous exports registration was a horrible idea, the following snippet should be an alternative with the added benefit of making sure that the scheduling will be in FIFO order in addition to making sure that it executes only after being properly initialized. Also the prior downside of triggering during initialization being ignored will be avoided through this change:

// Initialize the bed timeout holder object
var initPromise = beds.once("value", function (snapshot) {
    // your existing code...
});

// Frees a bed after a set amount of time
exports.scheduleFreeBed = functions.database.ref('/beds/{hotelIndex}/{bedIndex}/email').onUpdate( (snapshot, context) =>
    // make sure the scheduling happens after the initialization and in order
    // since his chaining doubles as a queue
    initPromise = initPromise.then(() => {
    // your existing code...
    })
);

Upvotes: 2

Related Questions