how to streamline/turn "on error, wait then retry once" routine into sub-function

328 views
Skip to first unread message

Edward Wu

unread,
Mar 10, 2023, 3:30:11 PM3/10/23
to google-apps-sc...@googlegroups.com
I have some functions that're time sensitive, and every once in a while, there's a Google hiccup, and the functions fail.

I've added this, which seems to help. Essentially, if there's an error, wait 30 seconds then rerun it once.

This helps with some of those hiccups.

const scriptPrp = PropertiesService.getScriptProperties();

function thisFunction() {
let currentRun = Number(scriptPrp.getProperty("runs"));

try {
// your main function would go here

scriptPrp.setProperty('runs', 0); // reset when successful run
} catch (e) {
scriptPrp.setProperty('runs', currentRun + 1);
var secondsToWaitUntilNextRun = 30;
Utilities.sleep(1000 * secondsToWaitUntilNextRun);

if (currentRun <= 2) {
thisFunction(); // If error rerun the function
} else {
scriptPrp.setProperty('runs', 0); // reset back to 0
throw `This still error'd out after multiple attempts. Error: ${e}`;
};
} // this closes the "try" routine
}

I'm wondering if there's a way to turn part of this into a Library function or a subfunction.
I know this won't work, but I'm thinking about something like this:

function thisFunction() {
let currentRun = Number(scriptPrp.getProperty("runs"));

try {
// your main function would go here

scriptPrp.setProperty('runs', 0); // reset when successful run
} catch (e) {
helperFunction(e); <-- this all gets moved to the helperFunction and I could keep the code streamlined
} // this closes the "try" routine
}

function helperFunction(e) {
scriptPrp.setProperty('runs', currentRun + 1);
var secondsToWaitUntilNextRun = 30;
Utilities.sleep(1000 * secondsToWaitUntilNextRun);
if (currentRun <= 2) {
thisFunction(); // If error rerun the function
} else {
scriptPrp.setProperty('runs', 0); // reset back to 0
throw `This still error'd out after multiple attempts. Error: ${e}`;
};
}

If I can do something like this, it'd make it a bit easier/cleaner to implement without having to copy-n-paste the "catch" code all the time.

Any suggestions?

Ed Sambuco

unread,
Mar 11, 2023, 3:19:16 PM3/11/23
to google-apps-sc...@googlegroups.com
As a suggestion, why not try to interpret the error object a bit more?   Google provides an "internal error" for the message out (sometimes).  

Yes, I occasionally see a Google internal error returned, especially for some calls to the Drive API.  And then too, sometimes the execution is just plain cancelled for no good reason.  When that happens, a manual rerun always works fine, but the try/catch logic doesn't catch it.

--
You received this message because you are subscribed to the Google Groups "Google Apps Script Community" group.
To unsubscribe from this group and stop receiving emails from it, send an email to google-apps-script-c...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/google-apps-script-community/CABNTzmAWz1fxhMqMUXH-y2sz3h98jfpNYwZkKrc%2BC%3DLaDXDX3Q%40mail.gmail.com.

Andrew Roberts

unread,
Mar 12, 2023, 4:26:33 PM3/12/23
to google-apps-sc...@googlegroups.com
FYI. This is Bruce McPherson's version that I use:

// Example
// -------

/*

var result = Utils_.rateLimitExpBackoff(
function() {return functionUnderTest()},
BACKOFF_WAIT_,
BACKOFF_TRIES_,
1,
true)

*/

// Exponetial Backoff
// ------------------

var BACKOFF_ENABLE_ = true
var BACKOFF_WAIT_ = 1000
var BACKOFF_TRIES_ = 5
var BACKOFF_MINIMUM_ERROR_LENGTH_ = 15
var BACKOFF_ON_ALL_ERRORS_ = false

var BACKOFF_ON_ERRORS = [
"Exception: Service invoked too many times",
"Exception: Rate Limit Exceeded",
"Exception: Quota Error: User Rate Limit Exceeded",
"Service error: Spreadsheets",
"Exception: Internal error. Please try again.",
"Exception: Cannot execute AddColumn because another task",
"Execution failed: Service invoked too many times in a short time",
"Exception: Failed to retrieve form data",
]

/**
*
* recursive rateLimitExpBackoff()
*
* @param {function} callBack some function to call that might return rate limit exception
* @param {number} [sleepFor=1000] optional amount of time to sleep for on the first failure in missliseconds
* @param {number} [maxAttempts=5] optional maximum number of amounts to try
* @param {number} [attempts=1] optional the attempt number of this instance - usually only used recursively and not user supplied
* @param {boolean} [optLogAttempts=false] log re-attempts to Logger
*
* @return {object} results of the callback
*/
var rateLimitExpBackoff = function(callBack, sleepFor, maxAttempts, attempts , optLogAttempts) {
Log_.functionEntryPoint()
var callingfunction = 'Utils.rateLimitExpBackoff()'
sleepFor = Math.abs(sleepFor || BACKOFF_WAIT_);
attempts = Math.abs(attempts || 1);
maxAttempts = Math.abs(maxAttempts || BACKOFF_TRIES_);
Log_.finest('sleepFor: ' + sleepFor)
Log_.finest('attempts: ' + attempts)
Log_.finest('maxAttempts: ' + maxAttempts)
Log_.finest('optLogAttempts: ' + optLogAttempts)
// Check properly constructed
Assert.assert(
callBack && typeof(callBack) === "function",
callingfunction,
'You need to specify a function for rateLimitBackoff to execute')

if (!BACKOFF_ENABLE_) {
return callBack()
}
// Try to execute it
try {
var r = callBack();
return r;
} catch(error) {
if (optLogAttempts) {
Log_.info("Backoff " + attempts + ": " + error)
}
// Failed due to rate limiting?
if (errorQualifies(error)) {
Assert.assert(
attempts <= maxAttempts,
callingfunction,
error + " (tried backing off " + (attempts - 1) + " times")
// Wait for some amount of time based on how many times
// we've tried plus a small random bit to avoid races
var wait = Math.pow(2,attempts) * sleepFor
var randomWait = Math.round(Math.random() * sleepFor)
Utilities.sleep(wait + randomWait);
Log_.finest('Sleep: ' + (wait + randomWait))
// Try again
return Utils_.rateLimitExpBackoff(callBack, sleepFor, maxAttempts, attempts + 1, optLogAttempts);
} else {
// Some other error
throw (error);
}
}

// Private Functions
// -----------------
/**
* Check if the error text matches that in the list
* of qualifying errors
*/
function errorQualifies (errorText) {
Log_.functionEntryPoint('Error text: ' + errorText)
if (BACKOFF_ON_ALL_ERRORS_) {
return true
}
var foundQualifyingError = BACKOFF_ON_ERRORS.some(function(nextErrorToCompare){
Log_.finest('nextErrorToCompare: ' + nextErrorToCompare)
var errorLongEnough = nextErrorToCompare.length > BACKOFF_MINIMUM_ERROR_LENGTH_
Log_.finest('errorLongEnough: ' + errorLongEnough)
var strippedText = errorText.toString().slice(0, nextErrorToCompare.length)
Log_.finest('stripped text: ' + strippedText)
return (errorLongEnough && (strippedText === nextErrorToCompare))
})
Log_.finest('Error qualifies: ' + foundQualifyingError)
return foundQualifyingError
} // Utils_.rateLimitExpBackoff.errorQualifies()
} // Utils_.rateLimitExpBackoff()


Edward Wu

unread,
Mar 13, 2023, 3:38:36 PM3/13/23
to google-apps-sc...@googlegroups.com
Reply all
Reply to author
Forward
0 new messages