Script lock for different users google app maker - google-apps-script

i'm struggling with problem of running script by 2 or more users in the same time. Script adds data to spreadsheet and creates folders in google drive.
I've tried using Lockservice, but it didn't work. And I've made my own 'Lock' which change value in sheet and when this Busy status is true script won't run, but it changes too slow.
function thisRunsClient(){
var lock = LockService.getScriptLock();
if(lock.hasLock()){return 'END')};
else{
lock.tryLock(10000)};
//rest of code
}

What I use is
var lock = LockService.getScriptLock();
try {
lock.waitLock(30000); // wait 30 seconds for others' use of the code section and lock to stop and then proceed
} catch (e) {
Logger.log('Could not obtain copy lock after 20 seconds.');
return;
}
//REST OF CODE
I'm not sure what you are trying to do with the haslock there, it looks to me like if you get the lock you immediately end the function, which doesn't seem like what you want to do.

Related

Google App Script Web app conflict when recording data on spreadsheet

I am building a user interface thanks to a GAS web app.
This script is connected to a spreadsheet to read and record data.
I am able to record data from the user entry to the spreadsheet thanks to my current code but my concern is about the reliability of this app when multiple users will be connected to this app and will try to record data.
In order to test the tool, I have created a 10 iterations loop on client side which send pre formatted data to the server side for recording into the spreadsheet.
When I launch the function on one computer it works (10 lines are properly recorded) but when a second user activate the same function on its session the total number of lines recorded is random (15 or 17 instead of 20).
When having a look to the spreadsheet when scriptS are running, I see that sometimes values on a line is overwritten by an other value (just like if the new row was not recorded at the proper place).
The Web app is share as execute as me (so the server side code is executed with my account)
In order to control what s going on, a lockservice has been implemented on the server side script (the part which records the data on the spreadsheet) and a new promise / async function on the client side.
function recordDataOnSheet(data){ // server side function to record data in the spreadsheet
var lock='';
lock = LockService.getScriptLock();
var success = lock.tryLock(10000);//Throws exception if fail wait 10s max to get a lock
if (success){
var dat =dataSheet.getDataRange().getValues();
lsRow++;
data[0].length);
dat.push(data);
dataSheet.getRange(1, 1, dat.length, dat[0].length).setValues(dat);
Logger.log(dat.length);
} else {Logger.log("Lock failed")};
lock.releaseLock();
} ```
and client side piece of script:
function runGoogleSript(serverFunc,dat){ // function to mange the asynchronous call of a server sdide function
return new Promise((resolve, reject) => {
google.script.run.withSuccessHandler(data => {
resolve(data)
}).withFailureHandler(er => {
reject(er)
})[serverFunc](dat)
});
}
async function test (){
for (i=0;i<10;i++){
let d = new Date();
setTimeout(() => { console.log(i) }, 2000);
try {
const data = await runGoogleSript("recordDataOnSheet",["06323","XX:",user.id,"2022-02-07", d.getMinutes() , d.getSeconds() ] );
}
catch (er) {alert(er);}
}
}
After dozen of test and tests, I have discovered that it was only an issue with the spreadsheet call.
As I am calling the same file/sheet in a loop sometimes the previous activity (to update the data in the sheet) were not over. This was at the origin of the issue.
I have simply added a SpreadsheetApp.flush(); at the end of my function recordDataOnSheet and it works and it is reliable

Why is the LockService is not working as intended?

I'm having a problem with the same entry begin saved multiple times and I realized it was mostly caused by double clicking. I'm trying to use LockService to avoid it: if the lock is not aquired in a millisecond the script should be aborted(because it's a duplicated operation).
//more code above
var lock = LockService.getScriptLock();
try{
lock.waitLock(1);//get the lock timing out in 1 millisecond
SpreadsheetApp.flush();
ss.insertRowBefore(6);
ss.getRange("A6").setValue(data[0][0]);
ss.getRange("B6").setValue(formatedString);
ss.getRange("C6").setValue(data[1][0]);
ss.getRange("D6").setValue(data[2][0]);
ss.getRange("E6").setValue(data[3][0]);
ss.getRange("F6").setValue(data[ref][0]);
SpreadsheetApp.flush();
Utilities.sleep(10);//This is to make sure it takes at least 1 millisecond
}
catch(e){
return;//It should generate a exception and end the script if the lock is not aquired
}
//more code bellow
The problem is that I still getting duplicated entries(tougth only 2 most of the time, so I believe it's working in part). What I am doing wrong?
Based on your sample sheet, you already read the data in the cells before you lock your succeeding code and clear its content.
Your original code:
var data = ss.getRange("B1:B2").getValues();
if(data[0][0] == "" || data[1][0] == "")
return;
var lock = LockService.getScriptLock();
try{
lock.waitLock(1);//get the lock timing out in 1 millisecond
SpreadsheetApp.flush();
ss.insertRowBefore(7);
ss.getRange("A7").setValue(data[0][0]);
ss.getRange("B7").setValue(data[1][0]);
SpreadsheetApp.flush();
Utilities.sleep(10);//This is to make sure it takes at least 1 millisecond
lock.releaseLock();
ss.getRange("B1:B2").setValue("");
}
catch(e){
return;//It should generate a exception and end the script if the lock is not aquired
}
What it does?
When you click submit button multiple times to execute your code, it will have n-times execution instance. As long as the clearing of cells don't take effect, each execution can write the data read from B1:B2.
Example:
Execution 1 started at 01:00:00.001 - already read the values in `B1:B2`
Execution 2 started at 01:00:00.005 - already read the values in `B1:B2`
Execution 3 started at 01:00:00.010 - already read the values in `B1:B2`
Execution 1 cleared B1:B2 content at 01:00:00.012. Hence you will have 3 copies of the submitted data. The writing of data in a new row was pended using the lock service, but the reading of data to add was not locked.
Solution
function submit() {
Utilities.sleep(1000);//simulate the upper part of the code
var ss = SpreadsheetApp.getActive().getSheetByName("spr1");
var lock = LockService.getScriptLock();
try{
lock.waitLock(1);//get the lock timing out in 1 millisecond
Logger.log("Locked: "+Utilities.formatDate(new Date(),Session.getScriptTimeZone(),"yyyy-MM-dd'T'HH:mm:ss.SSS"));
var data = ss.getRange("B1:B2").getValues();
Logger.log(data);
if(data[0][0] == "" || data[1][0] == "")
return;
SpreadsheetApp.flush();
ss.insertRowBefore(7);
ss.getRange("A7").setValue(data[0][0]);
ss.getRange("B7").setValue(data[1][0]);
Utilities.sleep(10);//This is to make sure it takes at least 1 millisecond
ss.getRange("B1:B2").setValue("");
SpreadsheetApp.flush();
lock.releaseLock();
Logger.log("UnLocked: "+Utilities.formatDate(new Date(),Session.getScriptTimeZone(),"yyyy-MM-dd'T'HH:mm:ss.SSS"));
}
catch(e){
return;//It should generate a exception and end the script if the lock is not acquired
}
Utilities.sleep(1000);//simulate the lower part of the code
}
Changes Done:
Lock the script first before reading the data in B1:B2
Make sure to clear the content of B1:B2 once it was added in a new row using flush() before releasing the lock.
Output:
Try this approach:
let lock = LockService.getScriptLock();
lock.tryLock(10000);
if (lock.hasLock()) {
ss.insertRowBefore(6);
ss.getRange(6, 1, 1, 6).setValues([data[0][0], formatedString, data[1][0], data[2][0]], data[3][0], data[ref][0])
SpreadsheetApp.flush();
lock.releaseLock();
}
I managed to solve my problem by adding a "flag" cell to count the numbers of active submissions. I'm using the lock only around it.
var rep = ss.getRange("C1");//the flag starting with 0
var lock = LockService.getScriptLock();
try{
lock.waitLock(1000);
var vAtual = rep.getValue();
if(vAtual >= 1)
return;//return hopefully if it already have one active submission
rep.setValue(vAtual+1);//increment for each active submit
SpreadsheetApp.flush();
lock.releaseLock();
}
catch(e){
return;
}
It's not pretty but it worked, so far. But I would still like to know why my original lock strategy failed. It can help some other people with similar problems too.

Terminate last execution using script

I have been testing a script for nearly 24 hours, the idea was that if certain cells were edited within the sheet, then the trigger would wait 5 minutes before resetting the values within those cells.
However when testing it became apparent that with many people using this sheet, the trigger was executing many times and sometimes the data edited by a user would reset within seconds because of a earlier execution.
To combat this I thought of putting a few lines of script in there, to cancel any previous executions of this function that is still running.
However with being a newbie to script and not having the knowledge I hope to gain, finding the information to start that script is proving difficult.
This is what I have so far
function RESET(e) {
const ar = e.range;
const as = ar.getSheet();
const cells = ["B3","E3"]; // add cells you want to be edited to activate the script
// if C3 or E3 is edited, wait for 300 seconds
if (as.getName()=="Gauge" && cells.includes(ar.getA1Notation())){
Utilities.sleep(300000);
as.getRange("B3").setValue("ALL");
as.getRange("E3").setValue("ALL");
}
}
You can use a document lock]1 to guard the RESET function running at the same time for multiple users, but this will probably lead to timeouts.
A better approach would be to create a time-driven trigger with a delay of 5 minutes inside the RESET function. When it fires, clear the values in B3/E3 or do any other work required.
Make sure that you clear previous triggers before you create new one, so that at any time only single trigger is active. If there is no user activity for 5 minutes, it will eventually run.
const SHEET = "Gauge"
// make sure to create as onEdit installable trigger, not a simple trigger
function onInstallableEdit(e) {
const ar = e.range;
const as = ar.getSheet();
const cells = ["B3","E3"]; // add cells you want to be edited to activate the script
// if C3 or E3 is edited, wait for 300 seconds
if (as.getName()==SHEET && cells.includes(ar.getA1Notation())){
const lock = LockService.getDocumentLock()
try{
lock.waitLock(10000)
// delete previous triggers, if any
ScriptApp.getProjectTriggers().filter(t=>t.getHandlerFunction()===myHandler.name).forEach(t=>ScriptApp.deleteTrigger(t))
// create new trigger to be fired after 5 minutes
ScriptApp.newTrigger(myHandler.name).timeBased().after(5*60*1000).create()
} catch(e){
console.warn(e)
}
}
}
function myHandler(){
const as = SpreadsheetApp.getActive().getSheetByName(SHEET)
as.getRange("B3").setValue("ALL");
as.getRange("E3").setValue("ALL");
}

Script running multiple times onFormSubmit

I have the following code set to run based on a onFormSubmit trigger but it will sometimes run multiple times with the same submission. I want to verify if it already copied the row and if so to stop the script.
function toDo(){
var responses = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Form Responses 1");
var jobs = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Jobs");
var lastrow = responses.getLastRow();
var col = responses.getLastColumn();
var row = responses.getRange(lastrow, 1, 1, 19).getValues();
jobs.appendRow(row[0]);
//copyValuesOnly(copyFromRange, copyToRangeStart);
var si = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Jobs');
var range = si.getRange("A2:R");
range.sort({column: 5,ascending: true}),({column: 1, ascending:true});
}
this is a known problem with GAS + Forms. The way that you solve it is by creating a script lock that rejects (causing them to return early) all other attempts within a period of time.
function toDo(){
SpreadsheetApp.flush();
var lock = LockService.getScriptLock();
try {
lock.waitLock(5000);
} catch (e) {
Logger.log('Could not obtain lock after 5seconds.');
return HtmlService.createHtmlOutput("<b> Server Busy please try after some time <p>")
// In case this a server side code called asynchronously you return a error code and display the appropriate message on the client side
return "Error: Server busy try again later... Sorry :("
}
var responses = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Form Responses 1");
var jobs = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Jobs");
var lastrow = responses.getLastRow();
var col = responses.getLastColumn();
var row = responses.getRange(lastrow, 1, 1, 19).getValues();
jobs.appendRow(row[0]);
//copyValuesOnly(copyFromRange, copyToRangeStart);
var si = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Jobs');
var range = si.getRange("A2:R");
range.sort({column: 5,ascending: true}),({column: 1, ascending:true});
Utilities.sleep(5000);
lock.releaseLock)();
}
I've had scripts that do this up to 8 times, and usually do it every 2-3 seconds. With this solution you are making a lock at the beginning and then sleeping at the end to make sure that the process time is greater than the wait time. (Here I used 5 seconds, that should prevent the double entry).
I have noticed that if you just make another copy of the sheet with the script, this error goes away. Seems to reset whatever was the issue in the original copy. Also try dis-abling the response receipts on your google forms.
How to check:
Go to script editor and check under execution logs. If you see multiple instances of on form submit, then you probably have multiple triggers somehow and the trigger is running multiple times legitimately.
To fix:
Go to triggers tab and delete all unwanted triggers.
Check code if you are creating new trigger through code. And comment that out.
Possible Reason:
I would have expected the code such as below to overwrite existing trigger. I had 19 triggers created. This was because every time I generated the link, it called Initialize and I got a new trigger added. Thus I could see script running 19 times.
const initialize = () => { const form = FormApp.getActiveForm(); //ScriptApp.newTrigger('onFormSubmit').forForm(form).onFormSubmit().create(); };
I noticed the problem got solved if I renamed the function called by onFormSubmit and saved the script.
If I click "run" in the script editor I get 2 emails per form submit. If I click "run" again I get 3 emails per form submit. I reset to only 1 email per form submit if I change the function name again.
Somehow when I run the script it duplicates the triggers.

Google Spreadsheets: Script to check for completion of ImportHTML

I am trying to scrape data of a website once day automatically. In Google Spreadsheets, i use the =ImportHTML() function to import data tables, and then I extract the relevant data with a =query(). These functions take between 10 and 30 seconds to complete calculation, every time I open the spreadsheet.
I use a scheduled Google Apps Script, to copy the data into a different sheet (where it is stored, so i can run statistics) every day.
My problem is that I am having trouble to make the script wait for the calculations to be finished, before the data is copied. The Result is that my script just copies the error Message "N/A".
I tried just adding a Utilities.sleep(60000);, but it didn't work.
Is it possible to create a loop, that checks for the calculation to finish? I tried this without success:
function checkForError() {
var spreadsheet = SpreadsheetApp.getActive();
var source = spreadsheet.getRange ("Today!A1");
if (source = "N/A") {
Utilities.sleep(6000);
checkForError();
} else {
moveValuesOnly();
}
}
Locks are for this. Look up lock services in the docs. Use a public lock.
Here's how I used Zig's suggestion (combined with my own check loop) to solve my similar problem:
// Get lock for public shared resourse
var lock = LockService.getPublicLock();
// Wait for up to 120 seconds for other processes to finish.
lock.waitLock(120000);
// Load my values below
// something like sheet.getRange("A1").setFormula('= etc...
// Now force script to wait until cell D55 set to false (0) before
// performing copy / pastes
var current = SpreadsheetApp.setActiveSheet(sheet.getSheets()[1]);
var ready = 1;
var count = 0;
while (true) {
// break out of function if D55 value has changed to zero or counter
// has hit 250
if (count >= 250) break;
// otherwise keep counting...
ready = current.getRange("D55").getValue();
if (ready == 0) {count = 400;}
Utilities.sleep(100);
++count;
}
// wait for spreadsheet to finish... sigh...
Utilities.sleep(200);
// Do my copy and pastes stuff here
// for example sheet.getRange("a1:b1").copyTo(sheet.getRange("a3"), {contentsOnly:true});
// Key cells are updated so release the lock so that other processes can continue.
lock.releaseLock();
// end script
return;
}
This has worked fantastic for me, stopped Google's sporadic service from ruining my work!
Thanks goes to Zig's suggestion!