Google App Script: LockService doesn't work - google-apps-script

I'm trying to implement a chat-bot using GAS (google app script). And faced some troubles with LockService. It seems not to work properly or at all.
When i get new message from appropriate chat, i saved delay-value (8 seconds) for ignoring subsequent messages from this chat. I do this using lockService to avoid skipping concurrent messages.
However it doesn't work, and some messages leaved out.
My code:
function get_message_safely(chat_id, message) {
var lock = LockService.getScriptLock();
const delay = 8*1000; // 8 seconds
// save time of getting message
var get_time = (new Date()).getTime();
// lock
// set lock before accessing data
try {
if (lock.hasLock()) { // ==always false (for unknown reasons)
sendLog('get_message_safely/hasLock', 'lock = true' + '\n' + message);
}
lock.waitLock(10000); // wait 10 seconds for others' use of the code section and lock to stop and then proceed
} catch (error) {
sendLog('get_message_safely/lock_service', 'ОШИБКА: ' + error);
send_message(chat_id, "Наши сервера сейчас загружены. Отправьте сообщение повторно или попробуйте позже.");
lock.releaseLock();
return;
}
// get data from PropertiesService
var scriptProperties = PropertiesService.getScriptProperties();
var chat_access_flag = scriptProperties.getProperty(chat_id);
if (chat_access_flag != undefined) { // if chat already exist
if (chat_access_flag == 'false') { // access denied
sendLog('get_message_safely/lock_service', 'LockService: доступ запрещен.\n' + message);
lock.releaseLock();
return;
}
var lock_time = Number(chat_access_flag);
if (get_time <= lock_time) { // access denied: lock_time is still actual
sendLog('get_message_safely/lock_service', 'LockService: доступ запрещен т.к. время не прошло:\n' + lock_time + ' - ' + get_time + ' = ' + (lock_time - get_time) + '\n' + message);
lock.releaseLock();
return;
}
sendLog('get_message_safely/lock_service', 'LockService: доступ разрешен:\n' + lock_time + ' - ' + get_time + ' = ' + (lock_time - get_time) + '\nnew_lock_time: ' + (get_time + delay) + '\n' + message);
}
scriptProperties.setProperty(chat_id, get_time + delay); // if access is allowed, set new delay
// lock release
lock.releaseLock(); // release the lock
get_message(chat_id, message); // handling message
}
So, sometimes i have the case (when i send a lot messages at the same time), when lockService should handle it but it doesn't:
Case (example):
// lock_time == a;
// get 1 message: access allowed. So set new value to lock_time (=b)
// lock_time == b;
// get 2 message: access denied. get_time < lock_time (=b)
// lock_time still == b;
// get 3 message: access allowed: get_time > lock_time, where lock_time == a (!???). So set new value to lock_time (=c);
// lock_time == с (??)
// get 4 message: access denied: get_time < lock_time (=b)
Handling messages due to lockService should be done sequentially. But some messages for some reason access to PropertiesService and get same date at same time.
It's blowing my mind. Have you any ideas what's happening here?
And how can i reach desirable result?

After studying your code I see that .hasLock() always returns false. Please, forgive me if I am mistaken. In your scenario this could mean that neither .waitLock() nor .tryLock() was called before. To fix the issue you have to use one of the mentioned methods. You can read on the links some example usages. Please, ask me any question if you need further help.

Related

How to prevent error throwing in Google Apps Script?

Please see the code herein under:
function binanceOrderBook() {
try {
muteHttpExceptions = true;
var workSpreadsheet = SpreadsheetApp.getActiveSpreadsheet();
var mySheet = workSpreadsheet.getSheetByName('Order Books');
if(mySheet == 'Sheet'){
mySheet.activate();
} else {
mySheet = workSpreadsheet.insertSheet('Order Books', 1).activate();
}
var ui = SpreadsheetApp.getUi();
var string = 'https://api.binance.com/api/v3/depth?';
var symbolResponse = ui.prompt('Pair Name', 'Please enter the pair symbol.\n\nExamples: BTCUSDT or ETHBTC:', ui.ButtonSet.OK_CANCEL);
var symbolButton = symbolResponse.getSelectedButton();
if(symbolButton == ui.Button.CANCEL){return}
var mySymbol = symbolResponse.getResponseText();
mySymbol = mySymbol.toUpperCase();
string = string + "symbol=" + mySymbol;
var limitResponse = ui.prompt('Limit:', 'Please enter Limit (Period Quantity).\nValid limits are:5, 10, 20, 50, 100, 500, 1000. \n Default limit is 100.\n You can leave it blank and simply click OK.', ui.ButtonSet.OK_CANCEL);
if(limitResponse.getSelectedButton() == ui.Button.CANCEL){return}
var myLimit = Number(limitResponse.getResponseText());
if(myLimit != 5 && myLimit != 10 && myLimit != 20 && myLimit != 50 && myLimit != 100 && myLimit != 500 && myLimit != 1000){myLimit = 100;}
string = string + "&limit=" + myLimit;
var myDate = new Date().toUTCString();
var jsonOrderBookData = JSON.parse(UrlFetchApp.fetch('https://api.binance.com/api/v3/depth?symbol=' + mySymbol + '&limit=' + myLimit));
reporter(jsonOrderBookData);
} catch (e){
exceptionHandler(e)
}
}
The problem I have is to run UrlFetchApp.fetch again when it encounters an error. I need to run it several times to get the result. So, I need to prevent the script from stopping when an error (code -1003) occurs, but how can I do that?
EDIT: There is a function windows.onerror in javascript which can be set to prevent the program from stopping. Is it useable in GAS? if yes, how? if No, is there a similar solution for GAS?
You could call binanceOrderBook() from within your catch statement. E.g.
...
} catch (e){
binanceOrderBook()
exceptionHandler(e)
}
Of course you probably should have some condition that exits the function if a certain error occurs, or if you know that the function needs to run no more than x number of times you could check that it has run less than x times before executing. For example,
const maxValue = 10 // whatever the max number of executions should be
function binanceOrderBook(executions) {
if (executions >= maxValue) return;
try {
...
} catch(e) {
binanceOrderBook((executions || 0) + 1));
exceptionHandler(e); // note that I am including this here because it's in your original example, but as it is written now, exception handler won't be called until binanceOrderBook executes without an error.
}
}
[Edit] To answer your second question, there is no equivalent to window.onerror that I know of in GAS. However, window.onerror is a global event handler and so would affect errors thrown by any functions defined in your project. To address a concern with a single function call like this, you are better off using a try catch statement as you have.

Gmail thread object unexpected behavior

I am trying to write a Google Apps Script that will process all emails that have a specific label.
I am using the GmailApp.search function to retrieve all of the relevant emails, but when I try to use the functions document in the GmailThread class, I get an error message that says that it can't find the function.
Here is my code;
var incoming = "To_Bot"
function readBotsEmail()
{
var emails = GmailApp.search("label:" + incoming);
Logger.log("This is the 'emails' object:" + emails)
var emailsLoopIndex = 0
for (var email in emails)
{
emailsLoopIndex += 1;
try
{
Logger.log("iteration " + emailsLoopIndex + " " + email.getMessageCount());
}
catch(e)
{
Logger.log("iteration " + emailsLoopIndex + " " + e);
}
}
}
Here is the logger output.
[14-01-26 03:40:00:909 EST] This is the 'emails' object:GmailThread,GmailThread
[14-01-26 03:40:00:911 EST] iteration 1 TypeError: Cannot find function getMessageCount in object 0.
[14-01-26 03:40:00:914 EST] iteration 2 TypeError: Cannot find function getMessageCount in object 1.
Where am I going wrong?
You should avoid using ambiguous variable names, "email" and "emails" are really bad choice when talking about threads on one side and index integer on the other...
Your issue comes mainly from this confusion between both variables, you used email instead of email*S* and also seem to forget that your value was an array of threads thus needing to be indexed.
Here is your working code, just one letter difference ;-) and a couple of brackets...
function readBotsEmail()
{
var emails = GmailApp.search("label:" + incoming);
Logger.log("This is the 'emails' object:" + emails)
var emailsLoopIndex = 0
for (var email in emails)
{
emailsLoopIndex += 1;
try
{
Logger.log("iteration " + emailsLoopIndex + " " + emails[email].getMessageCount());
}
catch(e)
{
Logger.log("iteration " + emailsLoopIndex + " " + e);
}
}
}
That said, you still have a lot of work on this script to make it return something interesting... for now it informs you on the number of threads and how many message they have... Anyway, that's a good start...
Good luck !

Intermittant DriveWriteVolume rateMax exception

I have been struggling with this error since about a week before DriveApp was released. I have a section of code that fails intermittantly with the error:
Service invoked too many times in a short time: driveWriteVolume rateMax. Try Utilities.sleep(1000) between calls.
Here is the code in question:
for(var a = 0; a<attachments.length; a++){
if(a > 0){
child = "." + (a + 1) + " ";
}
else{
child = ".1 ";
}
var parent = (m + 1);
Utilities.sleep(5000);
var file = attachmentFolder.createFile(attachments[a]);//This is the line that causes the error.
Utilities.sleep(1000);
file.rename(parent + child + attachments[a].getName());
}
I started with 1000ms, then gradually worked up to 5000 and it still throws the error every ~200 iterations. This is using DocsList.
I have had a similar problem in a script I'm working on.
The problem arises when I loop through the attachments to the message and attempt to save them too. If I skip the attachments, I don't have a problem, even with the same number of total file creations and the same sleep time between them. This is the code I'm using. newFolder.createFile(pdfBlob) does not cause a problem. newFolder.createFile(attachmentBlob) does.
// Create the message PDF inside the new folder
var htmlBodyFile = newFolder.createFile('body.html', messageBody, "text/html");
var pdfBlob = htmlBodyFile.getAs('application/pdf');
pdfBlob.setName(newFolderName + ".pdf");
newFolder.createFile(pdfBlob);
Utilities.sleep(sleepTime); // wait after creating something on the drive.
htmlBodyFile.setTrashed(true);
// Save attachments
Logger.log("Saving Attachments");
for(var i = 0; i < messageAttachments.length; i++) {
Logger.log("Saving Attachment " + i);
var attachmentBlob = messageAttachments[i].copyBlob();
newFolder.createFile(attachmentBlob);
Utilities.sleep(sleepTime); // wait after creating something on the drive.
} // each attachment

Unexpected exception upon serializing continuation Google Apps Script

I recently started getting the error "Unexpected exception upon serializing continuation" on a spreadsheet Google Apps Script when trying to debug. The error seem to start after I created a connection to the Google CloudSQL api. This error still occurs even after commenting out the jdbc object constructor. It appears that others have had this issue and needed a Google Tech to resolve the issue.
I have searched all of the discussion boards for a solution to this issue with no luck. Any chance there is a Google tech out there who could take a look under the hood for me? I would post code if I could determine what line was actually triggering the error.
EDIT:
Ok, I think I have discovered where the error is occuring. Seems to be the
var response = UrlFetchApp.fetch(url + nextPage,oauth_options);
in the while loop. Here is the entire function code.
function retrieveEvents(endTimeMinimum, updatedAfter, orderBy){
//var url = 'https://www.googleapis.com/calendar/v3/calendars/' + source_cal + '/events?key=' + api_key + "&futureevents=true&orderBy=updated&sortOrder=descending&updatedMin=" + last_sync_date_formated;
//var url = 'https://www.googleapis.com/calendar/v3/calendars/' + source_cal + '/events?key=' + api_key + "&orderBy=updated&sortOrder=descending&updatedMin=" + last_sync_date_formated;
var url = 'https://www.googleapis.com/calendar/v3/calendars/' + source_cal + '/events?key=' + api_key + "&singleEvents=true";
if ((orderBy != null) && (orderBy != "")){
url += "&orderBy=" + orderBy;
}
else url += "&orderBy=updated";
if ((updatedAfter != null) && (updatedAfter != "")){
url += "&updatedMin=" + updatedAfter;
}
else url += "&updatedMin=" + last_sync_dateTime;
//if no endTimeMinimum is specified, the current time will be used.
if (endTimeMinimum == null || endTimeMinimum == ""){
endTimeMinimum = date_rfc339("Today");
}
url += "&timeMin=" + endTimeMinimum;
Logger.log("Request URL:" + url);
var largeString = "";
var events = new Array();
var nextPage = "";
var jsonObj
while(true){
var response = UrlFetchApp.fetch(url + nextPage,oauth_options);
largeString = response.getContentText();
if ((largeString != null) && (largeString != "")) {
jsonObj = JSON.parse(largeString);
}
if ('items' in jsonObj) events = events.concat(jsonObj.items);
if ('nextPageToken' in jsonObj){
nextPage = "&pageToken=" + jsonObj.nextPageToken;
continue;
}
break;
}
if (events.length == 0)return null;
return events;
}
OK, so I was able to make the problem go away by removing the try catch block inside a function that was called from inside a try catch block in the main function. I no longer am seeing the "Unexpected exception upon serializing continuation" when running the program from the debugger.
I wish I had a more solid answer on what causes this error and how to correct it.
In my experience, this is not an error caused by that line (or any other) specifically, but because an error is triggered within a loop. I haven't pinned down the exact replicable cause, but GAS seems to lose the loop pointer certain errors.
The best I can suggest is that any line that you suspect to causing an error within the while loop wrap with a try-catch that logs the error to the logger and proceeds. The loop pointer is then not lost and will debug as expected.

typeof fails on trigger object property

I am trying to add an dumpObject function to a Spreadsheet Container bound Script.
Ideally, it is for visibility into variables passed through triggers.
I can run it all day long from within the Script Editor, but when setup as either an onEdit event or onEdit Installible trigger, it dies with no error.
I did some trial and error toast messages and confirmed the code in dumpObject is being executed from the Trigger.
If you take this code below, setup onEdit2 as an installable trigger, you might see it.
To see it work as a Trigger, uncommment the first line //e of onEdit2.
Best I can figure, is something in the e object coming from the trigger that is not quite what is expected of an object?
This test should be limiting the maxDepth to 5, so I don't think I'm hitting the 1000 depth limit.
UPDATE: The problem is calling typeof on the trigger object properties. For example, "typeof e.user" reports the following error: Invalid JavaScript value of type
Thanks,
Jim
function onEdit2(e) {
//e = {fish:{a:"1",b:"2"},range:SpreadsheetApp.getActiveSpreadsheet().getActiveRange(),B:"2"};
Browser.msgBox(typeof e);
Browser.msgBox("U:" + Utilities.jsonStringify(e));
e.range.setComment("Edited at: " + new Date().toTimeString());
Browser.msgBox("ShowOBJ:"+dumpObject(e, 5));
}
function dumpObject(obj, maxDepth) {
var dump = function(obj, name, depth, tab){
if (depth > maxDepth) {
return name + ' - Max depth\n';
}
if (typeof obj === 'object') {
var child = null;
var output = tab + name + '\n';
tab += '\t';
for(var item in obj){
child = obj[item];
if (typeof child === 'object') {
output += dump(child, item, depth + 1, tab);
} else {
output += tab + item + ': ' + child + '\n';
}
}
}
return output;
};
return dump(obj, '', 0, '');
}
You're not getting quite what you expect from the event object. If you throw in:
for(var q in e) {
Logger.log(q + " = " + e[q])
}
and then check the View->Logs menu item in the script editor you get
source = Spreadsheet
user = <your user>
So, checking the docs, you can come up with this as an alternative to your e.range.setComment("Edited at: " + new Date().toTimeString());:
e.source.getActiveSheet().getActiveCell().setComment("Edited at: " + new Date().toTimeString());
note: you can debug an error like you were (secretly) getting by wrapping your statement in a try catch like so:
try {
e.range.setComment("Edited at: " + new Date().toTimeString());
} catch (ex) {
Logger.log(ex);
}
and then checking the logs as mentioned above (or dumping to Browser.msgBox(), if you prefer).
This might not be a great "answer" but it works.
I found that replacing typeof with Object.prototype.toString.call(obj) I got something usable.
Of note, the e object returns [object Object] but the properties (e.user) return [object JavaObject]
if (Object.prototype.toString.call(obj).indexOf("object") != -1) {
var child = null;
var output = tab + name + '\n';
tab += '\t';
for(var item in obj){
child = obj[item];
if (Object.prototype.toString.call(child).indexOf("object") != -1) {
output += dump(child, item, depth + 1, tab);