I am using LockService to avoid duplicated actions, however I cannot make tryLock to fail during my testing.
Supposedly this code should write an error in ScriptProperties when running more than one time almost simultaneously, but it does not so far.
A second App instance should fail after tryLock for 1 second, while the first instance is sleeping for 15 seconds, right?
Any suggestions?
function doGet() {
testingLockService(1000, 15000);
return;
}
function testingLockService(trying, sleeping) {
var lock = LockService.getPrivateLock();
var hasMutex = lock.tryLock(trying);
if (hasMutex == false) { ScriptProperties.setProperty("LockService",new Date().toString()+" tryLock failed"); return; }
Utilities.sleep(sleeping);
lock.releaseLock();
return;
}
Interesting question. After having a little play with this I think the locking is working, it just appears it isn't because Google Apps Script does not seem to allow concurrent get requests, but rather queues them up. By moving your lock test to the server side it then works.
This is much easier to debug if you have your get request return something to the user rather than put it in a script property.
The following code will demonstrate the get requests being queued up. To test: make two concurrent requests, and look at the timestamps coming back, interesting you'll notice the second request will not have a start timestamp before the end timestamp of the first request, no matter how close together you make them. So the second request can perfectly validly get the lock. Here's the code:
function doGet() {
var app = UiApp.createApplication();
var tS = new Date();
var gotLock = testingLockService(0, 5000);
var tF = new Date();
var label = app.createLabel(gotLock ? 'Got the lock, and slept' : "Didn't get the lock");
app.add(label);
var label = app.createLabel('tS ' + tS.getTime());
app.add(label);
var label = app.createLabel('tF ' + tF.getTime());
app.add(label);
var label = app.createLabel('t delta ' + (tF - tS));
app.add(label);
return app;
}
function testingLockService(trying, sleeping) {
var lock = LockService.getPrivateLock();
var hasMutex = lock.tryLock(trying);
if (!hasMutex) { return false; }
Utilities.sleep(sleeping);
lock.releaseLock();
return true;
}
Now, to prove the locking does work, simply move the locking code to the server side. Again, to test, have two browser windows open and and click on both buttons. This time you will see the second request fail to get the lock and return immediately.
function doGet() {
var app = UiApp.createApplication();
var serverHandler = app.createServerHandler('doClick');
var button = app.createButton().setText("click me").addClickHandler(serverHandler);
app.add(button);
return app;
}
function doClick() {
var app = UiApp.getActiveApplication();
// code from here on is identical to previous example
var tS = new Date();
var gotLock = testingLockService(0, 5000);
var tF = new Date();
var label = app.createLabel(gotLock ? 'Got the lock, and slept' : "Didn't get the lock");
app.add(label);
var label = app.createLabel('tS ' + tS.getTime());
app.add(label);
var label = app.createLabel('tF ' + tF.getTime());
app.add(label);
var label = app.createLabel('t delta ' + (tF - tS));
app.add(label);
return app;
}
function testingLockService(trying, sleeping) {
var lock = LockService.getPrivateLock();
var hasMutex = lock.tryLock(trying);
if (!hasMutex) { return false; }
Utilities.sleep(sleeping);
lock.releaseLock();
return true;
}
Hopefully that has answered your question on the locking. Though it raises questions in my mind about the get request queueing. Is it only requests from the same user? I would love to hear from someone else if they have any more info on that, although, maybe that belongs in a question on its own.
Related
I've gotten my EmailPDF script to run correctly. But what I want is an alert to popup asking if you want to send the report.
function responseToSend() {
var spreadSheet=SpreadsheetApp.getActiveSpreadsheet();
var s = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Sheet5');
var se = s.getRange('A1').getValue();
var ui = SpreadsheetApp.getUi();
var prompt = ui.alert('Are you sure you wand to send this daily to ' + se + ' ?',ui.ButtonSet.YES_NO)
if(prompt == ui.Button.YES){(EmailPDF)
}
else{
ui.alert('Permission denied.');
}
}
function EmailPDF() {
var spreadSheet=SpreadsheetApp.getActiveSpreadsheet();
var s = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Sheet5');
var se = s.getRange('A1').getValue();
I first tried to write the ui.alert in my EmailPDF function. But I couldn't get it to work. I was able to get it to work if the response answer was YES, but when I would click NO it would still run the script. (i.e send the email)
I don't know what to up after "else {}" to get the script to stop running or return to the beginning.
Then I thought maybe I should create a function (responseToSend) that just runs the ui.alert and if the response is "yes" then run the EmailPDF function.
I'm sure it can be done both ways.
Try it this way:
function responseToSend() {
var spreadSheet=SpreadsheetApp.getActiveSpreadsheet();
var s = spreadSheet.getSheetByName('Sheet5');
var se = s.getRange('A1').getValue();
var ui = SpreadsheetApp.getUi();
var ps = Utilities.formatString('Are you sure you want send this daily to %s ?',se);
var prompt = ui.prompt(ps,ui.ButtonSet.YES_NO);
if(prompt.getSelectedButton() == ui.Button.YES){
EmailPDF();
}else{
ui.alert('Permission denied.');
}
}
I have a form with onSubmit trigger, Which will be used as an domain wide add - on.The add - on can be used
for many forms.
I need to disable the multiple submit triggers, instead run only 1 instance currently.
Say the form submissions arrive at same time, I need to avoid the duplicate instances of the script running when the current script is running.
I tried to use lock service
function onFormSubmit(e) {
// lock the document response
var lock = LockService.getDocumentLock();
if (lock.hasLock()) return;
//do some tasks
lock.releaseLock();
}
How do I make a script (addon) to run as a single instance.
UPDATE: This code too, doesnt achive the intended functionality
function OnSubmits(e) {
var releaseLock;
releaseLock = function() {
lock.releaseLock();
}
var lock = LockService.getDocumentLock();
lock.tryLock(1);
if (!lock.hasLock()) {
return;
}
var value = cUseful.Utils.expBackoff(function() {
releaseLock();
return PropertiesService.getScriptProperties().getProperty('key');
});
if (value == null) value = 0;
else value++;
cUseful.Utils.expBackoff(function() {
var folder = DriveApp.getFolderById("1wFGMh38JGarJd8CaiaOynlB7iiL_Pw6D");
folder.addFile(DriveApp.getFileById(DocumentApp.create('Document Name ' + value).getId()));
PropertiesService.getScriptProperties().setProperty('key', value);
});
releaseLock();
}
I am programming a Google Apps script within a spreadsheet. My use case includes iterating over a large set of folders that are children of a given one. The problem is that the processing takes longer than the maximum that Google allows (6 minutes), so I had to program my script to be able to resume later. I am creating a trigger to resume the task, but that is not part of my problem (at least, not the more important one at this moment).
My code looks like this (reduced to the minimum to illustrate my problem):
function launchProcess() {
var scriptProperties = PropertiesService.getScriptProperties();
scriptProperties.setProperty(SOURCE_PARENT_FOLDER_KEY, SOURCE_PARENT_FOLDER_ID);
scriptProperties.deleteProperty(CONTINUATION_TOKEN_KEY);
continueProcess();
}
function continueProcess() {
try {
var startTime = (new Date()).getTime();
var scriptProperties = PropertiesService.getScriptProperties();
var srcParentFolderId = scriptProperties.getProperty(SOURCE_PARENT_FOLDER_KEY);
var continuationToken = scriptProperties.getProperty(CONTINUATION_TOKEN_KEY);
var iterator = continuationToken == null ? DriveApp.getFolderById(srcParentFolderId).getFolders() : DriveApp.continueFolderIterator(continuationToken);
var timeLimitIsNear = false;
var currTime;
while (iterator.hasNext() && !timeLimitIsNear) {
var folder = iterator.next();
processFolder_(folder);
currTime = (new Date()).getTime();
timeLimitIsNear = (currTime - startTime >= MAX_RUNNING_TIME);
}
if (!iterator.hasNext()) {
scriptProperties.deleteProperty(CONTINUATION_TOKEN_KEY);
} else {
var contToken = iterator.getContinuationToken();
scriptProperties.setProperty(CONTINUATION_TOKEN_KEY, contToken);
}
} catch (e) {
//sends a mail with the error
}
}
When launchProcess is invoked, it only prepares the program for the other method, continueProcess, that iterates over the set of folders. The iterator is obtained by using the continuation token, when it is present (it will not be there in the first invocation). When the time limit is near, continueProcess obtains the continuation token, saves it in a property and waits for the next invocation.
The problem I have is that the iterator is always returning the same set of folders although it has been built from different tokens (I have printed them, so I know they are different).
Any idea about what am I doing wrong?
Thank you in advance.
It appears that your loop was not built correctly. (edit : actually, probably also another issue about how we break the while loop, see my thoughts about that in comments)
Note also that there is no special reason to use a try/catch in this context since I see no reason that the hasNext() method would return an error (but if you think so you can always add it)
here is an example that works, I added the trigger creation / delete lines to implement my test.
EDIT : code updated with logs and counter
var SOURCE_PARENT_FOLDER_ID = '0B3qSFd3iikE3MS0yMzU4YjQ4NC04NjQxLTQyYmEtYTExNC1lMWVhNTZiMjlhMmI'
var MAX_RUNNING_TIME = 5*35*6;
function launchProcessFolder() {
var scriptProperties = PropertiesService.getScriptProperties();
scriptProperties.setProperty('SOURCE_PARENT_FOLDER_KEY', SOURCE_PARENT_FOLDER_ID);
scriptProperties.setProperty('counter', 0);
scriptProperties.deleteProperty('CONTINUATION_TOKEN_KEY');
ScriptApp.newTrigger('continueProcess').timeBased().everyMinutes(10).create();
continueProcessFolder();
}
function continueProcessFolder() {
var startTime = (new Date()).getTime();
var scriptProperties = PropertiesService.getScriptProperties();
var srcParentFolderId = scriptProperties.getProperty('SOURCE_PARENT_FOLDER_KEY');
var continuationToken = scriptProperties.getProperty('CONTINUATION_TOKEN_KEY');
var iterator = continuationToken == null ? DriveApp.getFolderById(srcParentFolderId).getFolders() : DriveApp.continueFolderIterator(continuationToken);
var timeLimitIsNear = false;
var currTime;
var counter = Number(scriptProperties.getProperty('counter'));
while (iterator.hasNext() && !timeLimitIsNear) {
var folder = iterator.next();
counter++;
Logger.log(counter+' - '+folder.getName());
currTime = (new Date()).getTime();
timeLimitIsNear = (currTime - startTime >= MAX_RUNNING_TIME);
if (!iterator.hasNext()) {
scriptProperties.deleteProperty('CONTINUATION_TOKEN_KEY');
ScriptApp.deleteTrigger(ScriptApp.getProjectTriggers()[0]);
Logger.log('******************no more folders**************');
break;
}
}
if(timeLimitIsNear){
var contToken = iterator.getContinuationToken();
scriptProperties.setProperty('CONTINUATION_TOKEN_KEY', contToken);
scriptProperties.setProperty('counter', counter);
Logger.log('write to scriptProperties');
}
}
EDIT 2 :
(see also last comment)
Here is a test with the script modified to get files in a folder. From my different tests it appears that the operation is very fast and that I needed to set a quite short timeout limit to make it happen before reaching the end of the list.
I added a couple of Logger.log() and a counter to see exactly what was happening and to know for sure what was interrupting the while loop.
With the current values I can see that it works as expected, the first (and second) break happens with time limitation and the logger confirms that the token is written. On a third run I can see that all files have been dumped.
var SOURCE_PARENT_FOLDER_ID = '0B3qSFd3iikE3MS0yMzU4YjQ4NC04NjQxLTQyYmEtYTExNC1lMWVhNTZiMjlhMmI'
var MAX_RUNNING_TIME = 5*35*6;
function launchProcess() {
var scriptProperties = PropertiesService.getScriptProperties();
scriptProperties.setProperty('SOURCE_PARENT_FOLDER_KEY', SOURCE_PARENT_FOLDER_ID);
scriptProperties.setProperty('counter', 0);
scriptProperties.deleteProperty('CONTINUATION_TOKEN_KEY');
ScriptApp.newTrigger('continueProcess').timeBased().everyMinutes(10).create();
continueProcess();
}
function continueProcess() {
var startTime = (new Date()).getTime();
var scriptProperties = PropertiesService.getScriptProperties();
var srcParentFolderId = scriptProperties.getProperty('SOURCE_PARENT_FOLDER_KEY');
var continuationToken = scriptProperties.getProperty('CONTINUATION_TOKEN_KEY');
var iterator = continuationToken == null ? DriveApp.getFolderById(srcParentFolderId).getFiles() : DriveApp.continueFileIterator(continuationToken);
var timeLimitIsNear = false;
var currTime;
var counter = Number(scriptProperties.getProperty('counter'));
while (iterator.hasNext() && !timeLimitIsNear) {
var file = iterator.next();
counter++;
Logger.log(counter+' - '+file.getName());
currTime = (new Date()).getTime();
timeLimitIsNear = (currTime - startTime >= MAX_RUNNING_TIME);
if (!iterator.hasNext()) {
scriptProperties.deleteProperty('CONTINUATION_TOKEN_KEY');
ScriptApp.deleteTrigger(ScriptApp.getProjectTriggers()[0]);
Logger.log('******************no more files**************');
break;
}
}
if(timeLimitIsNear){
var contToken = iterator.getContinuationToken();
scriptProperties.setProperty('CONTINUATION_TOKEN_KEY', contToken);
scriptProperties.setProperty('counter', counter);
Logger.log('write to scriptProperties');
}
}
As of January 1, 2016 this is still a problem. The bug report lists a solution using the Advanced Drive API, which is documented here, under "Listing folders".
If you don't want to use Advanced services, an alternative solution would be to use the Folder Iterator to make an array of File Ids.
It appears to me that the Folder Iterator misbehaves only when created using DriveApp.continueFolderIterator(). When using this method, only 100 Folders are included in the returned Folder Iterator.
Using DriveApp.getFolders() and only getting Folder Ids, I am able to iterate through 694 folders in 2.734 seconds, according the Execution transcript.
function allFolderIds() {
var folders = DriveApp.getFolders(),
ids = [];
while (folders.hasNext()) {
var id = folders.next().getId();
ids.push(id);
}
Logger.log('Total folders: %s', ids.length);
return ids;
}
I used the returned array to work my way through all the folders, using a trigger. The Id array is too big to save in the cache, so I created a temp file and used the cache to save the temp file Id.
This is caused by a bug in GAS:
https://code.google.com/p/google-apps-script-issues/issues/detail?id=4116
It appears you're only storing a single continuation token. If you want to recursively iterate over a set of folders and allow the script to pause at any point (e.g. to avoid the timeout) and resume later, you'll need to store a bunch more continuation tokens (e.g. in an array of objects).
I've outlined a template that you can use here to get it working properly. This worked with thousands of nested files over the course of 30+ runs perfectly.
I have been playing with this small test code that - I admit - isn't very useful but I noticed that the value returned in the callBackElement of the first handler is undefined when this handler is called for the first time.
I couldn't figure out why... so I added a condition that solves the problem but I still would like to understand why this is working like that...
The script comes from a idea shown in this post earlier today, I commented the line that causes the error in the script below (it's a bit long, sorry about that) and runs as a sort of timer/counter to illustrate the ability to fire a handler programmatically with checkBoxes.
If someone can explain why this condition is necessary ?
var nn=0;
//
function doGet() {
var app = UiApp.createApplication().setHeight('120').setWidth('200').setTitle('Timer/counter test');
var Panel = app.createVerticalPanel()
var label = app.createLabel('Initial display')
.setId('statusLabel')
app.add(label);
var counter = app.createTextBox().setName('counter').setId('counter').setValue('0')
var handler1 = app.createServerHandler('loadData1').addCallbackElement(Panel);
var handler2 = app.createServerHandler('loadData2').addCallbackElement(Panel);
var chk1 = app.createCheckBox('test1').addValueChangeHandler(handler1).setVisible(true).setValue(true,true).setId('chk1');
var chk2 = app.createCheckBox('test2').addValueChangeHandler(handler2).setVisible(true).setValue(false,false).setId('chk2');
app.add(Panel.add(chk1).add(chk2).add(counter));
SpreadsheetApp.getActive().show(app)
}
function loadData1(e) {
var app = UiApp.getActiveApplication();
var xx = e.parameter.counter
//*******************************************************
if(xx){nn = Number(xx)}; // here is the question
// nn = Number(xx); // if I use this line the first occurence = undefined
nn++
var cnt = app.getElementById('counter').setValue(nn)
Utilities.sleep(500);
var chk1 = app.getElementById('chk1').setValue(false,false)
var chk2 = app.getElementById('chk2').setValue(true,true)
var label = app.getElementById('statusLabel');
label.setText("Handler 1 :-(");
return app;
}
function loadData2(e) {
var app = UiApp.getActiveApplication();
var xx = Number(e.parameter.counter)
xx++
var cnt = app.getElementById('counter').setValue(xx)
Utilities.sleep(500);
var chk1 = app.getElementById('chk1').setValue(true,true)
var chk2 = app.getElementById('chk2').setValue(false,false)
var label = app.getElementById('statusLabel');
label.setText("Handler 2 ;-)");
return app;
}
The app looks like this:
and is testable here
EDIT : working solution is to fire the handler after adding the widgets to the panel (see Phil's answer)
like this :
var chk1 = app.createCheckBox('test1').addValueChangeHandler(handler1).setVisible(true).setId('chk1');
var chk2 = app.createCheckBox('test2').addValueChangeHandler(handler2).setVisible(true).setId('chk2');
app.add(Panel.add(chk1).add(chk2).add(counter));
chk1.setValue(true,true);
chk2.setValue(false,false);
return app
The callback element which you specified for that handler (Panel) has no elements at the time that it is invoked. So you are essentially passing along a empty panel to that handler. So since chk1 hasn't been added to the panel yet, its value isn't added as a parameter to the handler.
Put chk1.setValue(true,true) after the call to Panel.add(chk1).
As seen in this example, the handler is queued when setValue(true,true) is called. This means that all the parameters that will be passed to the handler are gathered. It looks at the callback elements, reads their values, and then continues executing the doGet. After doGet finishes, the handler is executed.
UPDATE: Added doGet function.
I have a web app that is used for a registration process. I created an email confirmation that sends the user an email when they click the Register button.
My question is, how do I stop the function from running if the email field is left blank? At this point when I run the whole script, an error message is shown when I click register if I don't enter an email address. I don't want the function to throw an error and I want to be able to still register.
function doGet(e) {
var app = UiApp.createApplication().setTitle('Education Registration');
app.setStyleAttribute("background", "#DBE8C4");
var panel1 = app.createAbsolutePanel().setId('panel1');
panel1.setHeight(900);
panel1.setWidth(1500);
//Register button
var dateSelection = app.createButton('Register').setSize(140, 40);
var loadingWait = app.createLabel('After clicking Register, please allow 5 - 30 seconds for the webpage to process the request.');
var clickHandler = app.createServerHandler("respondToDateSelection");
dateSelection.addClickHandler(clickHandler);
clickHandler.addCallbackElement(panel1);
//Email Handler
var emailHandler = app.createServerHandler("emailConfirmation");
dateSelection.addClickHandler(emailHandler);
emailHandler.addCallbackElement(panel1);
return app;
}
function emailConfirmation(e) {
var app = UiApp.getActiveApplication();
app.getElementById('fNameText1');
app.getElementById('lNameText1');
app.getElementById('eAddressText1');
app.getElementById('dataItemsLB');
app.getElementById('aemailAddressText');
var fNameText1 = e.parameter.fNameText1;
var lNameText1 = e.parameter.lNameText1;
var eAddressText1 = e.parameter.eAddressText1;
var dataItemsLB = e.parameter.dataItemsLB;
var aemailAddressText = e.parameter.aemailAddressText;
var subject = "Class Registration Confirmation - " + fNameText1 + " " + lNameText1;
var emailBody = "This is an Email Confirmation.";
MailApp.sendEmail(eAddressText1, subject,
emailBody, {cc: aemailAddressText});
return app;
}
There are many ways to do that, the most "elegant" way would probably be using a client handler validator in your doGet function but you didn't show it ...(there is a specific validator for emails)
Another way is to have a warning label in your UI that is initially invisible and that gets visible when e.parameter.eAddressText1 is not a valid email or is empty, the same condition would apply to the sendEmail command and skip it if not valid then return to the UI.
feel free to post your doGet function to allow for more accurate answer.
EDIT :
thanks for posting your code, although it was incomplete and needed some work to make it an interesting example, I end up with this code to illustrate the use of clientHandlers, validators and try/catch for email adress...
Here is the code, I changed it to work on a spreadsheet to avoid deploying and versioning, it is just a test, nothing more...
function doGet(e) {
var app = UiApp.createApplication().setTitle('Education Registration');
app.setStyleAttribute("background", "#DBE8C4");
var panel0 = app.createFlowPanel().setId('panel0');
var panel1 = app.createVerticalPanel().setId('panel1');
var fNameText1=app.createTextBox().setName('fNameText1').setId('fNameText1');;
var lNameText1=app.createTextBox().setName('lNameText1').setId('lNameText1');;
var eAddressText1=app.createTextBox().setName('eAddressText1').setId('eAddressText1').setText('mail')
var dataItemsLB=app.createTextBox().setName('dataItemsLB').setId('dataItemsLB');;
var aemailAddressText=app.createTextBox().setName('aemailAddressText').setId('aemailAddressText').setText('mail');
panel1.add(fNameText1).add(lNameText1).add(eAddressText1).add(dataItemsLB).add(aemailAddressText)
panel0.add(panel1)
//Register button
var dateSelection = app.createButton('Register').setSize(140, 40).setId('dateSelection');
var loadingWait = app.createLabel('After clicking Register, please allow 5 - 30 seconds for the webpage to process the request.').setVisible(false).setId('loadingWait');
var clickHandler = app.createServerHandler("respondToDateSelection").validateEmail(eAddressText1);
dateSelection.addClickHandler(clickHandler);
clickHandler.addCallbackElement(panel0);
//Email Handler
var emailHandler = app.createServerHandler("emailConfirmation").validateEmail(eAddressText1);
dateSelection.addClickHandler(emailHandler);
emailHandler.addCallbackElement(panel1);
//client handlers
var warning = app.createLabel('Please enter your email where necessary').setId('warning').setVisible(false).setStyleAttribute('background','yellow')
var clientHandlerwait = app.createClientHandler().forTargets(loadingWait).setVisible(true).validateEmail(eAddressText1)
var clientHandler1 = app.createClientHandler().validateNotEmail(eAddressText1)
.forTargets(warning).setVisible(true).forEventSource().setStyleAttribute('color','red')
var clientHandler2 = app.createClientHandler().validateNotEmail(aemailAddressText)
.forTargets(warning).setVisible(true).forEventSource().setStyleAttribute('color','red')
dateSelection.addClickHandler(clientHandlerwait).addClickHandler(clientHandler1).addClickHandler(clientHandler2)
app.add(panel1.add(dateSelection).add(loadingWait).add(warning))
SpreadsheetApp.getActive().show(app)
// return app;
}
function respondToDateSelection(){
return
}
function emailConfirmation(e) {
var app = UiApp.getActiveApplication();
app.getElementById('warning').setVisible(false);
app.getElementById('dateSelection').setStyleAttribute('color','black')
var fNameText1 = e.parameter.fNameText1;
var lNameText1 = e.parameter.lNameText1;
var eAddressText1 = e.parameter.eAddressText1;
var dataItemsLB = e.parameter.dataItemsLB;
var aemailAddressText = e.parameter.aemailAddressText;
var subject = "Class Registration Confirmation - " + fNameText1 + " " + lNameText1;
var emailBody = "This is an Email Confirmation.";
try{
// MailApp.sendEmail(eAddressText1, subject,emailBody, {cc: aemailAddressText});
Utilities.sleep(500)// simulate a duration to read the message
app.getElementById('loadingWait').setText('mail sent').setVisible(true)
}catch(err){
app.getElementById('loadingWait').setText('error sending mail').setVisible(true)
}
return app;
}