Pages

Tuesday, 17 December 2024

Exponential Backoff

The following Google Apps Script is designed to explore Exponential Backoff - a process by which when something fails you try again a set number of times whilst increasing the delay between each attempt, up to a certain point.

I needed this for a tool I built which adds Guests to a Calendar event from a Google Form submission. Whilst I was using ScriptLock to prevent simultaneous submissions, the code ran so fast that it would infrequently trip the Calendar API with the following error message "API call to calendar.events.patch failed with error: Rate Limit Exceeded".

By infrequently I mean a reported issue only once in 3,500 submissions over the course of 12 months. Enough however to take the opportunity to learn about Exponential Backoff and to squash that single instance.

Just a note that this is one way to implement it.

Sample Apps Script code for Exponential Backoff
Sample Apps Script code for Exponential Backoff


The Code

There are a few elements of the code I want to draw out specifically, such as 'count' which is a variable we create to determine how many times we should iterate through our 'while()' loop when retrying the failed process - set to 3 in this example. Then there is our 'backOff' variable which is what we use for 'Utilities.sleep()' to determine how long we want to delay the process by - set to 2 seconds in this example (the actual value it requires needs to be in milliseconds):
// how many times the 'while' loop should iterate through
var count = 3;

// the delay in how long the code should wait before retrying, in milliseconds
var backOff = 2000;  // 2 seconds

In order to generate the specific error message I receive when updating Google Calendar events quicker than the API allows, I use the 'Math.random()' JavaScript method to generate a random number that I then test against a particular value. In this example by using '0.5' I create a 50% chance of the error message being shown via 'throw new Error'. You could tweak this to '0.9' for instance to create a 90% chance of seeing the error message if you require for your testing:
if (Math.random() < 0.5) {

    throw new Error("API call to calendar.events.patch failed with error: Rate Limit Exceeded");

};
We wrap the error in a try/catch to then allow us to perform a JavaScript 'match' on it to check it's the actual error message we are expecting. You can read about filtering a try/catch error message here. If we find a match in the error message we need to decrement our 'count' variable by 1 to acknowledge we've gone through the process once. We then need to check the value of this variable to determine if it has reached 0 - in which case we want to 'throw' the error message back to our Parent Function as we have reached the limit on how many times we wanted the while() loop to occur. Or we want our code implement the pause ('backOff') to slow it down, remembering to double the length of this afterwards (for instance 2 seconds to 4 seconds to 8 seconds ... etc):

// decrement count by 1;
count -= 1;

// if count is 0 then number of iterations has been reached
if (count == 0) {

    // return error message to 'catch' of Parent Function
    throw (error);

} else {

    // pause code for backoff period before trying again
    Utilities.sleep(backOff);

    // double length of backoff period each time
    backOff *= 2;

};


Download

Exponential Backoff download (please use 'Overview' > 'Make a copy' for your own version).

/**
* Code to learn about Exponential Backoff.
* Developed when experiencing 'Rate Limit' issue with updating a Calendar Event of Attendees.
*
* DEVELOPED BY THE GIFT OF SCRIPT: https://www.pbainbridge.co.uk/
*/
function mainFunction() {
console.log("Starting the process");
try {
// how many times the 'while' loop should iterate through
var count = 3;
// the delay in how long the code should wait before retrying, in milliseconds
var backOff = 2000; // 2 seconds
// call Function
var returnMessage = testBackOff(count, backOff);
// only displayed if previous Function completed successfully without error/throw
console.log("returnMessage is: " + returnMessage);
} catch (error) {
console.log("Catch error is: " + error.stack);
};
};
function testBackOff(count, backOff) {
// loop through for the number of times specified in Parent Function
while (count > 0) {
try {
console.log("count is: " + count);
// create a defined error message with 50% chance (0.5) of triggering when the code is run
if (Math.random() < 0.5) {
throw new Error("API call to calendar.events.patch failed with error: Rate Limit Exceeded");
};
return "Successfully updated Calendar Attendees.";
} catch (error) {
// convert 'catch' message to string and perform JavaScript 'match' for keywords
var errorString = error.toString();
var matching = errorString.match(/Rate Limit Exceeded/gi);
// test if 'match' is true to display specific log message
if (matching) {
// log message
console.log('Message match found');
// decrement count by 1;
count -= 1;
// if count is 0 then number of iterations has been reached
if (count == 0) {
// return error message to 'catch' of Parent Function
throw (error);
} else {
// pause code for backoff period before trying again
Utilities.sleep(backOff);
// double length of backoff period each time
backOff *= 2;
};
} else {
// log message
console.log('Different message found.');
throw (error);
};
};
};
};

No comments:

Post a Comment