Search code examples
javascriptnode.jspromisegoogle-cloud-functionsidempotent

Google Cloud Functions - Retry


I have this idempotent function with multiple promises that I wrote for Google Cloud Functions. I want to have retry enabled since my used API is pretty inconsistent. This requires a rejected promise to be returned when a retry is needed.

Therefore I tried to return a promise.all([]) but that does not terminate/stop the function when one of the promises fails. It then even proceeds to the promise.all().then()? This should only happen when all 4 promises are successful.

Who can point me in the right direction? Does it even make sense what I'm trying?

exports.scheduleTask = functions
  .firestore.document("tasks_schedule/{servicebonnummer}")
  .onCreate((snap, context) => {

    servicebonnummer = snap.data().data.servicebonnummer;
    bondatum = snap.data().data.bondatum;
    servicestatus = snap.data().data.servicestatus;
    tijdstip = snap.data().data.tijdstip;
    firestorePromise = null;
    firestoreFinish = null;
    cashPromise = null;
    cashFinish = null;

    //Firebase
    //firestoreSchedule executes the required promise
    //checkFinished points to a database where it checks a boolean for idempotency
    //firestoreFinish writes to this database and sets the boolean to true when the promise is successful

    if (!checkFinished("tasks_schedule", servicebonnummer, "firestore")) {
      firestorePromise = scheduleFirestore(
        servicebonnummer,
        bondatum,
        servicestatus,
        tijdstip
      )
        .then(output => {
          firestoreFinish = markFinished(
            "tasks_schedule",
            servicebonnummer,
            "firestore"
          );
          return output;
        })
        .catch(error => {
          console.error(
            "scheduleFirestore - Error connecting to Firestore: ",
            error
          );
          return error;
        });
    }

    //SOAP API
    //cashSchedule executes the required promise
    //checkFinished points to a database where it checks a boolean for idempotency
    //cashFinish writes to this database and sets the boolean to true when the promise is successful

    if (!checkFinished("tasks_schedule", servicebonnummer, "cash")) {
      cashPromise = scheduleCash(
        servicebonnummer,
        moment(bondatum),
        servicestatus,
        tijdstip
      )
        .then(result => {
          if (result[0].response.code === "2") {
            cashFinish = markFinished(
              "tasks_schedule",
              servicebonnummer,
              "cash"
            );
            return result;
          }
          throw new Error("Validation error, response not successful");
        })
        .catch(error => {
          console.error("scheduleCash - Error connecting to CASH API: ", error);
          return error;
        });
    }

    //CHECK PROMISES

    return Promise.all([
      firestorePromise,
      firestoreFinish,
      cashPromise,
      cashFinish
    ])
      .then(result => {
        removeTask("tasks_schedule", servicebonnummer);
        return result;
      })
      .catch(error => {
        console.error("scheduleTask - Retry: ", error);
        return error;
      });
  });

Solution

  • If you code:

    let somePromise = new Promise(...);
    return somePromise.then(funcA).catch(funcB);
    

    Then you are indeed returning a promise. However, since you have handlers for that promise in your code, we need to look at what happens in more detail. Let us assume that somePromise is rejected. This will mean that the catch() handler will be invoked. It is the outcome of that catch handler that will be the ultimate resolution of the returned promise.

    If we look at the MDN docs for Promise.catch() we find the following:

    The Promise returned by catch() is rejected if onRejected throws an error or returns a Promise which is itself rejected; otherwise, it is resolved.

    If we look at your code,

    catch(error => {
      console.error("scheduleTask - Retry: ", error);
      return error;
    });
    

    And now ask:

    1. Does this code throw an error? Nope ... it has no throw statement in it and hence just returns the values passed in.
    2. Does the code return a Promise? Nope ... it is passed an error value and simply returns that error value which I am pretty sure will not itself be a Promise.

    This means that the overall Promise returned is concluded in a resolved state and not a rejected state and hence the overall Cloud Function is considered to have concluded and is not retried.

    Your options may be:

    catch(error => {
      console.error("scheduleTask - Retry: ", error);
      throw error;
    });
    

    or

    catch(error => {
      console.error("scheduleTask - Retry: ", error);
      return Promise.reject(error);
    });
    

    References: