-
-
Save areeves1992/bf82d3d28bfb9af5c8f3b4e857b44782 to your computer and use it in GitHub Desktop.
ContinuousBatchLibrary is a Google Apps Script library that manages large batches and works around the 5 minute limitation of GAS execution. It does this by setting time based triggers in the future as well as memorising the last processed key in the batch in order to restart from the correct position. At the end of the batch a cleanup function …
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* --- Continous Execution Library --- | |
* | |
* Copyright (c) 2013 Patrick Martinent | |
* | |
* Licensed under the Apache License, Version 2.0 (the "License"); | |
* you may not use this file except in compliance with the License. | |
* You may obtain a copy of the License at | |
* | |
* http://www.apache.org/licenses/LICENSE-2.0 | |
* | |
* Unless required by applicable law or agreed to in writing, software | |
* distributed under the License is distributed on an "AS IS" BASIS, | |
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
* See the License for the specific language governing permissions and | |
* limitations under the License. | |
*/ | |
/************************************************************************* | |
* Call this function at the start of your batch script | |
* it will create the necessary UserProperties with the fname | |
* so that it can keep managing the triggers until the batch | |
* execution is complete. It will store the start time for the | |
* email it sends out to you when the batch has completed | |
* | |
* @param {fname} str The batch function to invoke repeatedly. | |
* the batch function name is one that we want to call again and again, but his comments don't make that too clear... - AMR | |
*/ | |
function startOrResumeContinousExecutionInstance(fname){ | |
//Actually, instead of using UserProperties, we're going ot use ScriptProperties because there is a bug in | |
//Google that stops a user from seeing their userProperties and changing them at the time this code was written. | |
//this gets our properties. I kept the variable called userproperties because by the time I realized it was an issue, | |
//I used this all over the code and this was easier. | |
var userProperties = PropertiesService.getScriptProperties(); | |
//This get the value associated with the key called "GASCBL_functionnamehere_START_BATCH" | |
//so we can see when we started this batch | |
var start = userProperties.getProperty('GASCBL_' + fname + '_START_BATCH'); | |
//if that value isn't anything to begin with, we should set it. | |
if (start === "" || start === null){ | |
//Here we are renaming the var start with todays date | |
//Remember, this is using JavaScripts Date functions/object. | |
//http://www.w3schools.com/jsref/jsref_obj_date.asp | |
start = new Date(); | |
//now we need to set the property with the key "GASCBL_functionnamehere_START_BATCH" and the value as the date. | |
userProperties.setProperty('GASCBL_' + fname + '_START_BATCH', start); | |
//we also need to set the key to "" or blank because we clearly haven't started anything yet. | |
userProperties.setProperty('GASCBL_' + fname + '_KEY', ""); | |
}//end of the if statement | |
//This sets the key "GASCBL_functionnamehere_START_ITERATION" with the date. | |
//The difference is that the batch was this was the first time this was run - this is the start of our batch. | |
//The second time this function is called, we only need to reset the iteration, or the first second third, etc | |
//time we need to run the code again | |
userProperties.setProperty('GASCBL_' + fname + '_START_ITERATION', new Date()); | |
//Delete the old trigger name to keep things clean | |
//You can only have ~40 triggers, so keep track of them. | |
deleteCurrentTrigger_(fname); | |
//Now we need to start the second trigger for this function, and we should wait 420000 ms, or 7 minutes. | |
//The code will stop around 4.5 minutes so there is a 3.5 minute difference from when one stops and another stops to give the code wait time to catch up. | |
//This probably could be shorter, but I didn't want to push it. | |
enableNextTrigger_(fname,420000); | |
}//end of startOrResumeContinousExecutionInstance function | |
/************************************************************************* | |
* In order to be able to understand where your batch last executed you | |
* set the key ( or counter ) everytime a new item in your batch is complete | |
* when you restart the batch through the trigger, use getBatchKey to start | |
* at the right place | |
* | |
* @param {fname} str The batch function we are continuously triggering. | |
* @param {key} str The batch key that was just completed. | |
*/ | |
function setBatchKey(fname, key){ | |
//We need to get our properties again | |
var userProperties = PropertiesService.getScriptProperties(); | |
//Now, we are setting the key with the spot (or usually i,j,or k) we were at in our loop | |
userProperties.setProperty('GASCBL_' + fname + '_KEY', key); | |
//We also need to sleep for 1000ms or .016 minutes because Google kept telling me I called this too many times one after another | |
Utilities.sleep(1000); | |
}//end of function setBatchKey | |
/************************************************************************* | |
* This function returns the current batch key, so you can start processing at | |
* the right position when your batch resumes from the execution of the trigger | |
* | |
* @param {fname} str The batch function we are continuously triggering. | |
* @returns {string} The batch key which was last completed. | |
*/ | |
function getBatchKey(fname){ | |
//getting our properties again... | |
var userProperties = PropertiesService.getScriptProperties(); | |
//Now, all we are doing is returning the key or iterator (i,j, or k) we were at at | |
return userProperties.getProperty('GASCBL_' + fname + '_KEY'); | |
}//end of function getBatchKey | |
/************************************************************************* | |
* When the batch is complete run this function, and pass it an email and | |
* custom title so you have an indication that the process is complete as | |
* well as the time it took | |
* | |
* @param {fname} str The batch function we are continuously triggering. | |
* @param {emailRecipient} str The email address to which the email will be sent. | |
* @param {customTitle} str The custom title for the email. | |
*/ | |
function endContinuousExecutionInstance(fname, emailRecipient, customTitle){ | |
//we need to get our properties again | |
var userProperties = PropertiesService.getScriptProperties(); | |
//The end will be right now because this is only called when we are done with the batch. | |
var end = new Date(); | |
//Now we are getting the start time of the entire batch | |
var start = userProperties.getProperty('GASCBL_' + fname + '_START_BATCH'); | |
//We're also getting the key we stopped at | |
var key = userProperties.getProperty('GASCBL_' + fname + '_KEY'); | |
//This is the subject for the email we are going to send to ourselves | |
var emailTitle = customTitle + " : Continuous Execution Script for: " + fname; | |
//This is the body of the email we are sending ourselves | |
//We are including the start time, the end time and the key | |
//The "<br>"s are HTML for breaks so it looks clean. | |
var body = "Started : " + start + "<br>" + "Ended :" + end + "<br>" + "LAST KEY : " + key + "<br>"; | |
//We're deleting our trigger so it stays clean. | |
deleteCurrentTrigger_(fname); | |
//And deleting all the properties we used | |
userProperties.deleteProperty('GASCBL_' + fname + '_START_ITERATION'); | |
userProperties.deleteProperty('GASCBL_' + fname + '_START_BATCH'); | |
userProperties.deleteProperty('GASCBL_' + fname + '_KEY'); | |
userProperties.deleteProperty('GASCBL_' + fname); | |
}//end of function endContinuousExecutionInstance | |
/************************************************************************* | |
* Call this function when finishing a batch item to find out if we have | |
* time for one more. if not exit elegantly and let the batch restart with | |
* the trigger | |
* | |
* @param {fname} str The batch function we are continuously triggering. | |
* @returns (boolean) whether we are close to reaching the exec time limit | |
*/ | |
function isTimeRunningOut(fname){ | |
//getting the properties | |
var userProperties = PropertiesService.getScriptProperties(); | |
//we're both getting the property for the iteration and turning it into a date | |
var start = new Date(userProperties.getProperty('GASCBL_' + fname + '_START_ITERATION')); | |
//Setting the variable to the date and time now | |
var now = new Date(); | |
//this is finding out the number of seconds from now to when we started and rounding it. | |
var timeElapsed = Math.floor((now.getTime() - start.getTime())/1000); | |
//This will return a true if the time passed since starting the iteration is larger than 4.5 minutes | |
return (timeElapsed > 270); | |
}//end of function isTimeRunningOut | |
/* | |
* Set the next trigger, 7 minutes in the future | |
*/ | |
function enableNextTrigger_(fname, time) { | |
//getting the properties | |
var userProperties = PropertiesService.getScriptProperties(); | |
//making the next trigger which is time based after the time you need into the function in ms | |
var nextTrigger = ScriptApp.newTrigger(fname).timeBased().after(time).create(); | |
//this is just getting the id so we can store it. | |
//to be honest, I don't think we need this anymore, but I'm afraid to change it... | |
var triggerId = nextTrigger.getUniqueId(); | |
//this sets the key with the function name with the value of the trigger id | |
userProperties.setProperty('GASCBL_' + fname, triggerId); | |
} | |
/* | |
* Deletes the current trigger, so we don't end up with undeleted | |
* time based triggers all over the place | |
*/ | |
function deleteCurrentTrigger_(fname) { | |
//getting those properties | |
var userProperties = PropertiesService.getScriptProperties(); | |
//now we're just getting the triggerID again | |
//not sure if this even has to be in here, but I'm afraid to remove these... | |
var triggerId = userProperties.getProperty('GASCBL_' + fname); | |
//the var triggers is now an object with ALL our triggers, not just the ones we made | |
//This was the down fall of the original code. | |
var triggers = ScriptApp.getProjectTriggers(); | |
//first of all we need to go backwards because we're deleting stuff | |
for (var i = triggers.length -1 ; i >= 0; i--) { | |
//We need to make sure that we actually have triggers to begin with and if we don't, we need to break (or return out of the loop) | |
//This actually may not be necessary, considering if we have no triggers, var i will be -1 on the first pass, | |
//but the original code has no checks so we need to double check. | |
if (triggers[i] == '' || triggers[i] === null || triggers.length == 0){ | |
break; | |
}//end of if | |
//now we need to make sure that the trigger at i matches the function name we are trying to delete | |
//The handler function returns the name of the function it is running | |
//This was not in the original code and threw everything off because it deleted everything | |
//We also need to make sure we don't delete the original triggers that will keep this thing going every day. | |
if (triggers[i].getHandlerFunction() == fname){ | |
//We should sleep before we call this so Google doesn't yell at us | |
Utilities.sleep(1000); | |
//now we delete it! | |
ScriptApp.deleteTrigger(triggers[i]); | |
}//end of if | |
//now we need to set the function name to blank | |
userProperties.setProperty('GASCBL_' + fname, ""); | |
}//end of for loop | |
}//end function deleteCurrentTrigger_ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function testCBL() { | |
// simulate a trigger on batch process, i.e if you run the batch | |
// everyday at a particular time | |
var triggerId = ScriptApp.newTrigger("batchProcess").timeBased().after(60 * 1000).create(); | |
// clean test by deleting it the initial triggers | |
Utilities.sleep(90000); | |
ScriptApp.deleteTrigger(triggerId); | |
} | |
function batchProcess() { | |
// initiate CBL for the function | |
CBL.startOrResumeContinousExecutionInstance("batchProcess"); | |
// this is approach is valid is we are looking to process a for loop | |
// this is because the key start value is "" | |
if (CBL.getBatchKey("batchProcess") === "") | |
CBL.setBatchKey("batchProcess", 0); | |
// find out where we left off (again with a for loop) | |
var counter = Number(CBL.getBatchKey("batchProcess")); | |
for(var i = 0 + counter; i < 80; i++) { | |
// perform batch | |
Utilities.sleep(5000); | |
Logger.log("batchProcess_" + i) | |
CBL.setBatchKey("batchProcess", i); | |
// find out wether we have been running this iteration for more that 5 minutes | |
// in which case exit the batch elegantly | |
if (CBL.isTimeRunningOut("batchProcess")) | |
return; | |
} | |
// if we get to this point, it means we have completed the batch and we can cleanup | |
// this will also send an email with the last batch key, start and end times | |
CBL.endContinuousExecutionInstance("batchProcess", "[email protected]", "My Custom Email Title"); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment