Disable multiple instances of onSubmit trigger during submissions - google-apps-script

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();
}

Related

How do I prevent multiple users from running Google Apps script code at the same time in a Google Sheet?

I am sharing a google script sheet to multiple users, I just want to prevent the same user to run the script at the same time. What do I have to do? Is it by code or by settings only?
As #TheMaster said in a comment, LockService is the way to go. Specifically, you want LockService.getDocumentLock() or LockService.getScriptLock() depending on how your script is set up.
Example of how to use the lock service in Apps Script:
var timeoutMs = 5000; // wait 5 seconds before giving up
var lock = LockService.getScriptLock(); // or LockService.getDocumentLock();
lock.tryLock(timeoutMs);
try {
if (!lock.hasLock()) {
var msg = 'Skipped execution after ' + (timeoutMs / 1000).toString() + ' seconds; script already running.';
console.log(msg);
try {
// If script is being run from a user UI context (ex: from a Menu option),
// gracefully alert the user we gave up
SpreadsheetApp.getUi().alert(msg);
} catch (e2) {}
} else {
//*** ADD CODE YOU WANT TO PROTECT HERE
}
} catch (e) {
console.error(e);
try {
var ui = SpreadsheetApp.getUi();
ui.alert("Error", e ? e.toString() : e, ui.OK);
} catch (e2) {}
}
SpreadsheetApp.flush();
lock.releaseLock();

Trigger on calculated date on Google AppMaker

I have function called alertnotice(to, message, body) that will be executed on a user onClick() event. The function will execute sendEmail(to, message, body) to send the email base on a calculated trigger as in variable triggerDay such as below:
function alertnotice(to, subject, body, dueDate, notifyBeforeDay) {//start of this class
function sendEmail(to, subject, body){
try {
MailApp.sendEmail({
to: to,
subject: subject,
htmlBody: body
});
} catch(e) {
console.error(JSON.stringify(e));
}
}
//check if there is an existing trigger for this process
var existingTrigger = PropertiesService.getScriptProperties().getProperty("sendEmailTrigger");
//set the renewal notice day
var triggerDay = (dueDate - notifyBeforeDay) / (1000*60*60*24);
//if the trigger already exists, inform user about it
if(existingTrigger) {
return "Alert notice had been sent";
} else { // if the trigger does not exists, continue to set the trigger to send alert notice
//runs the script every day at 1am on the time zone specified
var newTrigger = ScriptApp.newTrigger('sendEmail')
.timeBased()
.atTime(triggerDay)
.create();
var triggerId = newTrigger.getUniqueId();
if(triggerId) {
PropertiesService.getScriptProperties().setProperty("autoExportTrigger", triggerId);
return "Alert notice send successfully!";
} else {
return "Failed to send alert notice. Try again please";
}
}
}//end of this class
So for example, if the dueDate is 30/07/2018 and the notifyBeforeDay = 30, the function should send the email 30 days before the due date. I tried to achieve that but not so sure whether my algorithm will work. Can anyone give advice on this?
This implementation looks fragile to me. I would rather go with single trigger to avoid any possible duplicated emails and ensure at least one. Smth like this:
// I am server script, trigger me on schedule (for instance nightly)
function sendAlerts() {
var query = app.models.Customers.newQuery();
// query only for non-notified customers with upcoming due date
query.filters.NorificationAttempts._lessThan = ATTEMPTS_THRESHOLD;
query.filters.Notified._equals = false;
query.filters.DueDate._lessThan = <dueDate>;
var customers = query.run();
customers.forEach(function(customer) {
var success = sendNotification(customer);
if (success) {
customer.Notified = true;
} else {
customer.NorificationAttempts++;
}
// performance trade off in favor of consistency
app.saveRecords([customer]);
});
}
Trigger for such script can be installed by app's admin, you can find similar implementation in People Skills template.

Checking a Google Calendar Event's Existence

I have a very basic problem within my whole code which became a head-ache for a couple of days. I have made an extensive research on this issue however couldn't be able to find an exact solution (or I have missed the one's that I might have found). Here it is:
I have a spreadsheet, where I log in Google Calendar events with their properties including the id of the event. Sometimes, I clean up the Google Calendar manually and just want to run a code which checks the events existence in the calendar and if not delete the row. My code is:
function cleanCal(calid, eventid) {
var calid = 'my calendar id';
var eventid = 'event id';
var event = CalendarApp.getOwnedCalendarById(calid).getEventById(eventid);
if (event) {
event.deleteEvent();
// clean-up sheet
} else {
// clean-up sheet
}
}
Basically if the event is in the calendar, the code shall delete it first and then clean the sheet, if it's not there, it should clean the sheet only. However, when the code is executed, though the calendar event is not there, the if statement returns true and raises an error when trying to delete the event as it's actually not there. I haven't been able to find out the reason, how and why the event object return true though the event is not existing. Where am I doing wrong?? Thanks for replying and any help is really appreciated.
[EDIT]
This is the code that I use to check events existence with Calendar API v3
function verifyCalendarEvent_OLD(calid, eventid) {
var cal = CalendarApp.getCalendarById(calid)
var exists = true;
var response = Calendar.Events.list(
calid, {
showDeleted: true,
fields: "items(id,status,summary)"
}
);
for (var i = 0; i < response.items.length; i++) {
if (response.items[i].id == eventid.split("#")[0] && response.items[i].status == "cancelled") {
exists = false;
break;
}
}
return exists;
}
How about this answer?
Modification points :
For example, if there is no event of the event ID, CalendarApp.getOwnedCalendarById(calid).getEventById(eventid) returns null. When this is evaluated by if, that is used as false.
So I thought that you might try to retrieve the event which has already been removed. Because at Google Calendar, even if the event is removed, the event ID is remained. So although there is no event for the event ID, if (event) {} in your script returns true. I confirmed that CalendarApp.getOwnedCalendarById(calid).getEventById(eventid) retrieves the removed events. For this situation, you can know whether the event is removed by confirming the event status.
When the event status is confirmed, it indicates that the event is still not removed.
When the event status is cancelled, it indicates that the event has already been removed.
Preparation to use this modified sample script :
When you use this modified script, please enable Calendar API at Advanced Google Services and API console.
Enable Calendar API v3 at Advanced Google Services
On script editor
Resources -> Advanced Google Services
Turn on Calendar API v3
Enable Calendar API at API console
On script editor
Resources -> Cloud Platform project
View API console
At Getting started, click Enable APIs and get credentials like keys.
At left side, click Library.
At Search for APIs & services, input "Calendar". And click Calendar API.
Click Enable button.
If API has already been enabled, please don't turn off.
When you run this script, if an error occurs, you might be required to wait few minutes until the API is enabled.
Modified script :
function cleanCal(calid, eventid) {
var calid = 'my calendar id';
var eventid = 'event id';
var status = Calendar.Events.get(calid, eventid).status; // Added
if (status == "confirmed") { // Modified
var event = CalendarApp.getOwnedCalendarById(calid).getEventById(eventid); // Added
event.deleteEvent();
// clean-up sheet
} else {
// clean-up sheet
}
}
If I misunderstand your question, I'm sorry.
Edit :
In my environment, the events removed by manual and script can be retrieved as status=cancelled. Since I didn't know your situation, I prepared a sample script. This sample script is a simple flow.
Create new event.
Delete the created event.
Confirm the deleted event.
Here, your additional script was used.
Sample script :
function deleteeventa() {
// Create new event
var calid = 'my calendar id';
var c = CalendarApp.getCalendarById(calid);
var r = c.createEvent("sample event for deleting", new Date(2018,0,11,00,00), new Date(2018,0,11,01,00));
// Delete the created new event
var eventid = r.getId().split('#')[0];
var event = c.getEventById(eventid);
event.deleteEvent();
// Your additional script
var exists = true;
var response = Calendar.Events.list(
calid, {
showDeleted: true,
fields: "items(id,status,summary)"
}
);
for (var i = 0; i < response.items.length; i++) {
if (response.items[i].id == eventid && response.items[i].status == "cancelled") {
exists = false;
break;
}
}
Logger.log("%s, %s, %s", r.getTitle(), r.getId(), exists)
}
Result :
sample event for deleting, ######google.com, false
Following Tanaike's great help, I have come up with this code snippet which - as far as I have tested - works fine for now. Hope it might be helpful for other users. Here is the code fragment:
function verifyCalendarEvent(calid, eventid) {
var cal = CalendarApp.getCalendarById(calid)
eventid = eventid.split("#")[0];
var exists = true;
var eventIds = [];
var eventStats = [];
var response = Calendar.Events.list(
calid, {
showDeleted: true,
fields: "items(id,status,summary)"
}
);
for (var i = 0; i < response.items.length; i++) {
eventIds.push(response.items[i].id);
}
for (var i = 0; i < response.items.length; i++) {
eventStats.push(response.items[i].status);
}
if (eventIds.indexOf(eventid) > 0) {
for (var i = 0; i < eventIds.length; i++) {
if (eventIds[i] == eventid && eventStats[i] == "cancelled") {
exists = false;
}
}
} else {
exists = false;
}
Logger.log("Calendar Event ["+eventid+"] exists? >> "+exists);
return exists;
}

Cannot make LockService tryLock to Fail

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.

concurrency issue and loosing form data in a google apps script powered google form

I've been recently having trouble with what I believe to be a concurrency issue when people are submitting the form near the same times, which is resulting in lost data for a google form. I'm already using the Lock service to prevent this issue, but I still seem to have problems. http://googleappsdeveloper.blogspot.com/2011/10/concurrency-and-google-apps-script.html
The form currently has onFormSubmit triggers: formSubmitReply and logMessage. formSubmitReply sends a confirmation to people that submitted the form and logMessage is supposed to back up the information in a separate spreadsheet in the case that rows in the regular spreadsheet get clobbered. It should be extracting the values from the formSubmit event and then appending it to the "log" sheet.
I've included all the current code for the script and replaced emails with place holders. Can I get some help identify anything buggy in the code that could be preventing the form from recording rows in the form?
function getColIndexbyName(colName){
var sheet=SpreadsheetApp.getActiveSheet();
var rowWidth=sheet.getLastColumn();
var row=sheet.getRange(1,1,1,rowWidth).getValues();//this is the first row
for ( i in row[0]){
var name=row[0][i];
if(name == colName || new RegExp(colName,'i').test(name)){
return parseInt(i)+1;
}
}
return -1
}
function makeReceipt(e){
/*This is for Student Volunteer auto-confirmation*/
var ss,sheet, rowWidth, headers, rowWidth,curRow, values, greeting, robot, msg, space, newline;
curRow=e.range.getRow();
ss=SpreadsheetApp.getActiveSpreadsheet();
sheet=ss.getSheetByName("RAW");
rowWidth=sheet.getLastColumn();
headers=sheet.getRange(1,1,1,rowWidth).getValues();
values=sheet.getRange(curRow,1,1,rowWidth).getValues();
greeting='Hi '+sheet.getRange(curRow,getColIndexbyName('First Name'),1,1).getValue()+"! <br><br>"+ ' ';
robot="<i>Below are the responses you submitted. Please let us know if any changes arise!</i> <br><br>";
msg=greeting+robot;
space=' ';
newline='<br>';
for( i in headers[0]){
//only write non "Reminders" column values
if(headers[0][i]!="Reminders"){
msg+="<b>";
msg+=headers[0][i];
msg+="</b>";
msg+=":";
msg+=space;
msg+=values[0][i];
msg+=newline;
}
}
return msg;
}
/**
* Triggered on form submit
**/
function formSubmitReply(e) {
var ss, row, mailIndex, userEmail, message, appreciation;
var lock = LockService.getPublicLock();
if(lock.tryLock(60000)){
try{
ss=SpreadsheetApp.getActiveSheet();
row=e.range.getRow();
mailIndex=getColIndexbyName('Email Address');
userEmail=e.values[mailIndex-1];
message=makeReceipt(e);
MailApp.sendEmail(userEmail, 'BP Day 2012 Confirmation for'+' '+userEmail,message,{name:"Name", htmlBody:message, replyTo:"example#example.com"});
messageAlert100(e);
} catch(err){
e.values.push("did not send email");
MailApp.sendEmail(""example#example.com","error in formSubmitReply"+err.message, err.message);
}
logToSpreadsheet(e);
} else {
//timeOut
try{
if(e && e.values){
logToSpreadsheet(e);
e.values.push("did not send email");
}
}catch(err){
MailApp.sendEmail("example#example.com", "error in logging script block "+err.message, err.message)
}
}
}
/**
* Triggered on form submit
**/
function messageAlert100(e){
var cheer_list, curRow, cheer_list, cheer_index, cheer, ss=SpreadsheetApp.getActiveSpreadsheet();
if(e && e.range.activate){
curRow=e.range.getRow();
}
cheer_list=["Congratulations!", "Give yourself a pat on the back!", "Yes!", "Cheers!","It's time to Celebrate!"];
cheer_index=Math.floor(Math.random()*cheer_list.length);
cheer=cheer_list[cheer_index];
if(typeof(curRow) != "undefined" && curRow % 100 ==0){
MailApp.sendEmail("example#example.com", ss.getName()+": "+cheer+" We now have "+ curRow + " Volunteers!", cheer+" We now have "+ curRow + " Volunteers!");
}
}
/**
*
**/
function logToSpreadsheet(e){
var ss=SpreadsheetApp.getActiveSpreadsheet(), sh;
if(!ss.getSheetByName("log")){
sh=ss.insertSheet("log");
}
sh=ss.getSheetByName("log");
if(e && e.values !==null){
sh.appendRow(e.values)
} else {
sh.appendRow(e);
}
Logger.log(e);
}
There is a very simple approach that I use to avoid concurrency issues with forms, I had to imagine that before GAS offered the lock method.
Instead of using the on form submit trigger I use a timer trigger (every few minutes or so) on a function that checks a column for a flag (MAIL SENT) ... if the flag is not present I send the email with processed data, copy to the backup sheet and set the flag. I do this on every row starting from the last one and stop when I find the flag. This way I'm sure all datarows are processed and no duplicate mail is sent.
It is actually very simple to implement, your script will need only a few modifications.
Viewed from the users side the result is nearly the same as they receive a mail just a few minutes after their submission.
EDIT : of course in this setup you cannot use e parameter to get the form data but you'll have to read data on the sheet instead... but that's not really a big difference ;-)