Skip to content

Commit

Permalink
Merge pull request #1 from RomainVialard/RomainVialard-patch-1
Browse files Browse the repository at this point in the history
Retry on UrlFetch calls with muteHttpExceptions
  • Loading branch information
RomainVialard authored Apr 4, 2018
2 parents 931a40d + cde94a1 commit e259f2e
Showing 1 changed file with 109 additions and 42 deletions.
151 changes: 109 additions & 42 deletions src/ErrorHandler.gs.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* And makes sure that caught errors are correctly logged in Stackdriver
*
* expBackoff()
* urlFetchWithExpBackOff()
* logError()
*
* _convertErrorStack()
Expand All @@ -32,58 +33,123 @@
* @return {*} - The value returned by the called function.
*/
function expBackoff(func) {

// execute func() then retry 5 times at most if errors
for (var n = 0; n < 6; n++) {
try {
return func();
// actual exponential backoff
n && Utilities.sleep((Math.pow(2, n-1) * 1000) + (Math.round(Math.random() * 1000)));

var response = undefined;
var error = undefined;

var noError = true;
var isUrlFetchResponse = false;

// Try / catch func()
try { response = func() }
catch(err) {
error = err;
noError = false;
}


// Handle retries on UrlFetch calls with muteHttpExceptions
if (noError && typeof response.getResponseCode === "function") {
isUrlFetchResponse = true;

var responseCode = response.getResponseCode();

// Only perform retries on error 500 for now
if (responseCode === 500) {
error = response;
noError = false;
}
}
catch(e) {
if (e.message) {
// Check for errors thrown by Google APIs on which there's no need to retry
// eg: "Access denied by a security policy established by the administrator of your organization.
// Please contact your administrator for further assistance."
if (e.message.indexOf('Invalid requests') !== -1
|| e.message.indexOf('Access denied') !== -1
|| e.message.indexOf('Mail service not enabled') !== -1) {
throw e;
}

// TODO: YAMM specific ?: move to YAMM code
else if (e.message.indexOf('response too large') !== -1) {
// Thrown after calling Gmail.Users.Threads.get()
// maybe because a specific thread contains too many messages
// best to skip the thread
return null;
}

// Return result that is not an error
if (noError) return response;


// Process error retry
if (!isUrlFetchResponse && error.message) {
// Check for errors thrown by Google APIs on which there's no need to retry
// eg: "Access denied by a security policy established by the administrator of your organization.
// Please contact your administrator for further assistance."
if (error.message.indexOf('Invalid requests') !== -1
|| error.message.indexOf('Access denied') !== -1
|| error.message.indexOf('Mail service not enabled') !== -1) {
throw error;
}

if (n === 5) {
// 'User-rate limit exceeded' is always followed by 'Retry after' + timestamp
// Maybe we should parse the timestamp to check how long we need to wait
// and if we should abort or not
if (e.message && e.message.indexOf('User-rate limit exceeded') !== -1) {
ErrorHandler.logError(e, {
shouldInvestigate: true,
failedAfter5Retries: true,
context: "Exponential Backoff"
});
return null;
}

// Investigate on errors that are still happening after 5 retries
// Especially error "Not Found" - does it make sense to retry on it?
ErrorHandler.logError(e, {
failedAfter5Retries: true,
context: "Exponential Backoff"
});

throw e;
// TODO: YAMM specific ?: move to YAMM code
else if (error.message.indexOf('response too large') !== -1) {
// Thrown after calling Gmail.Users.Threads.get()
// maybe because a specific thread contains too many messages
// best to skip the thread
return null;
}
}

}


// Action after last re-try
if (isUrlFetchResponse) {

ErrorHandler.logError(new Error(response.getContentText()), {
shouldInvestigate: true,
failedAfter5Retries: true,
urlFetchWithMuteHttpExceptions: true,
context: "Exponential Backoff"
});

return response;
}

else {
// 'User-rate limit exceeded' is always followed by 'Retry after' + timestamp
// Maybe we should parse the timestamp to check how long we need to wait
// and if we should abort or not
// 'User Rate Limit Exceeded' (without '-') isn't followed by 'Retry after' and it makes sense to retry
if (error.message && error.message.indexOf('User-rate limit exceeded') !== -1) {
ErrorHandler.logError(error, {
shouldInvestigate: true,
failedAfter5Retries: true,
context: "Exponential Backoff"
});

Utilities.sleep((Math.pow(2, n) * 1000) + (Math.round(Math.random() * 1000)));
return null;
}

// Investigate on errors that are still happening after 5 retries
// Especially error "Not Found" - does it make sense to retry on it?
ErrorHandler.logError(error, {
failedAfter5Retries: true,
context: "Exponential Backoff"
});

throw error;
}
}

/**
* Helper function to automatically handles exponential backoff on UrlFetch use
*
* @param {string} url
* @param {Object} params
*
* @return {UrlFetchApp.HTTPResponse} - fetch response
*/
function urlFetchWithExpBackOff(url, params) {
params = params || {};

params.muteHttpExceptions = true;

return ErrorHandler.expBackoff(function(){
return UrlFetchApp.fetch(url, params);
});
}

/**
* If we simply log the error object, only the error message will be submitted to Stackdriver Logging
* Best to re-write the error as a new object to get lineNumber & stack trace
Expand Down Expand Up @@ -142,6 +208,7 @@ function logError(e, additionalParams) {
this['ErrorHandler'] = {
// Add local alias to run the library as normal code
expBackoff: expBackoff,
urlFetchWithExpBackOff: urlFetchWithExpBackOff,
logError: logError
};

Expand Down

0 comments on commit e259f2e

Please sign in to comment.