Skip to content

Instantly share code, notes, and snippets.

@patt0
Last active March 7, 2024 20:12
Show Gist options
  • Star 38 You must be signed in to star a gist
  • Fork 10 You must be signed in to fork a gist
  • Save patt0/8395003 to your computer and use it in GitHub Desktop.
Save patt0/8395003 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 …
/**
* --- 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.
*/
function startOrResumeContinousExecutionInstance(fname){
var userProperties = PropertiesService.getUserProperties();
var start = userProperties.getProperty('GASCBL_' + fname + '_START_BATCH');
if (start === "" || start === null)
{
start = new Date();
userProperties.setProperty('GASCBL_' + fname + '_START_BATCH', start);
userProperties.setProperty('GASCBL_' + fname + '_KEY', "");
}
userProperties.setProperty('GASCBL_' + fname + '_START_ITERATION', new Date());
deleteCurrentTrigger_(fname);
enableNextTrigger_(fname);
}
/*************************************************************************
* 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){
var userProperties = PropertiesService.getUserProperties();
userProperties.setProperty('GASCBL_' + fname + '_KEY', key);
}
/*************************************************************************
* 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){
var userProperties = PropertiesService.getUserProperties();
return userProperties.getProperty('GASCBL_' + fname + '_KEY');
}
/*************************************************************************
* 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){
var userProperties = PropertiesService.getUserProperties();
var end = new Date();
var start = userProperties.getProperty('GASCBL_' + fname + '_START_BATCH');
var key = userProperties.getProperty('GASCBL_' + fname + '_KEY');
var emailTitle = customTitle + " : Continuous Execution Script for " + fname;
var body = "Started : " + start + "<br>" + "Ended :" + end + "<br>" + "LAST KEY : " + key;
MailApp.sendEmail(emailRecipient, emailTitle, "", {htmlBody:body});
deleteCurrentTrigger_(fname);
userProperties.deleteProperty('GASCBL_' + fname + '_START_ITERATION');
userProperties.deleteProperty('GASCBL_' + fname + '_START_BATCH');
userProperties.deleteProperty('GASCBL_' + fname + '_KEY');
userProperties.deleteProperty('GASCBL_' + fname);
}
/*************************************************************************
* 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) wether we are close to reaching the exec time limit
*/
function isTimeRunningOut(fname){
var userProperties = PropertiesService.getUserProperties();
var start = new Date(userProperties.getProperty('GASCBL_' + fname + '_START_ITERATION'));
var now = new Date();
var timeElapsed = Math.floor((now.getTime() - start.getTime())/1000);
return (timeElapsed > 270);
}
/*
* Set the next trigger, 7 minutes in the future
*/
function enableNextTrigger_(fname) {
var userProperties = PropertiesService.getUserProperties();
var nextTrigger = ScriptApp.newTrigger(fname).timeBased().after(7 * 60 * 1000).create();
var triggerId = nextTrigger.getUniqueId();
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) {
var userProperties = PropertiesService.getUserProperties();
var triggerId = userProperties.getProperty('GASCBL_' + fname);
var triggers = ScriptApp.getProjectTriggers();
for (var i in triggers) {
if (triggers[i].getUniqueId() === triggerId)
ScriptApp.deleteTrigger(triggers[i]);
}
userProperties.setProperty('GASCBL_' + fname, "");
}
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 &lt; 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", "you@example.com", "My Custom Email Title");
}
@jyosingh
Copy link

how to use this library simply we have to add this library in the our project that's it or we have to do anything else?

@patt0
Copy link
Author

patt0 commented Aug 14, 2014

@jyosing please have a look at the blog posts that describes how this works, let me know if you have any more issues.

@raghav1191
Copy link

Does this code need me to click on the run button everytime the execution time is exceeded?

@jamonroad
Copy link

I'm confused as to how to use your library. I've looked at your example on and off for quite some time and even tried it with some of our code but haven't been able to figure it out.

One point of confusion for me in your example is the use of the word 'batchProcess' for your sample. It seems like it's calling its self since the sample function name is 'batchProcess' and the library functions called within its scope are also calling 'batchProcess'.

I have a function that imports and parses groups of CSV files and writes them to individual Google Sheet pages. Some of these data sets are so large that our script times out before completing all the imports.

Our function is called get(); , if using our function in your example, what would I put where you have "function batchProcess() { "?

@jbbrower
Copy link

jbbrower commented Aug 4, 2015

Looks like you will need to update to version 4 for the deprecated userProperties stuff:
https://developers.google.com/apps-script/reference/properties/

Looks like an easy fix of: getUserProperties() then getProperties() or setProperties() on that fetched properties set.

We now get this message when using the library:
UserProperties API is deprecated.
File: Code Line: 54
The API has been marked as deprecated which means that the feature should be avoided and may be removed in the future. Consider using an alternative solution.

@kcsf
Copy link

kcsf commented Jul 13, 2016

@patt0 - you say, "have a look at the blog posts that describes how this works"
where are the blog posts?

@kcsf
Copy link

kcsf commented Jul 13, 2016

@patt0 - we would very much like to implement this on our geocoding script. would you please have look and advise?
mlucool/geocode-google-sheets#1

@areeves1992
Copy link

@patt0 - I changed the CBL you have provided a bit in my project I am working on. I have forked your gist and edited the code, especially the delete function. Would you mind taking a look at what I've added? Thanks for the code! It is very helpful.

@yanshiyason
Copy link

Thank you for this library!

@gchristofferson
Copy link

Awesome library, thank you! When I was using it I see that the UserProperties API is deprecated. Are there any plans to work in an alternative solution to this library?

@fgala
Copy link

fgala commented Nov 6, 2017

@olawalejuwonm
Copy link

Please i need help on how to use this

@YehudaBialik
Copy link

This is a really great Library and a great help! I suggest adding

CBL.setBatchKey(fname, i + 1);

in cblTest.js between lines 31 and 32 in order to avoid processing the same data twice.

It looks like this:

if (CBL.isTimeRunningOut("batchProcess")) {
  CBL.setBatchKey(fname, i + 1);     
  return;
}

and of course designating ContinuousBatchLibrary as CBL.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment