I have the below code. I want the function called execute2 to run after the function execute. When I put the script task inside the first function and don't include execute2 then it all runs fine and the second separate script runs. But when I run it in the below format it does not. What is preventing execute 2 from actually running here?
Thank you
/**
* #NApiVersion 2.x
* #NScriptType ScheduledScript
* #NModuleScope SameAccount
*/
define(['N/task'],
/**
* #param {record} record
* #param {search} search
*/
function(task) {
var FILE_ID = 433961;
var SEARCH_ID = 1610;
function execute(scriptContext) {
var searchTask1 = task.create({
taskType: task.TaskType.SEARCH
});
searchTask1.savedSearchId = SEARCH_ID;
searchTask1.fileId = FILE_ID;
var searchTaskId1 = searchTask1.submit();
//
//Next Task
//
FILE_ID = 433963;
SEARCH_ID = 1606;
var searchTask2 = task.create({
taskType: task.TaskType.SEARCH
});
searchTask2.savedSearchId = SEARCH_ID;
searchTask2.fileId = FILE_ID;
var searchTaskId2 = searchTask2.submit();
//
//Next Task
//
FILE_ID = 434489;
SEARCH_ID = 1637;
var searchTask3 = task.create({
taskType: task.TaskType.SEARCH
});
searchTask3.savedSearchId = SEARCH_ID;
searchTask3.fileId = FILE_ID;
var searchTaskId3 = searchTask3.submit();
//
// Next Task
//
FILE_ID = 434490;
SEARCH_ID = 1606;
var searchTask4 = task.create({
taskType: task.TaskType.SEARCH
});
searchTask4.savedSearchId = SEARCH_ID;
searchTask4.fileId = FILE_ID;
var searchTaskId4 = searchTask4.submit();
}
function execute2(scriptContext){
var scriptTask = task.create({
taskType: task.TaskType.SCHEDULED_SCRIPT,
scriptId: "customscript_email_sender",
deploymentId: "customdeploy_email_sender_d"
});
var scriptTaskId = scriptTask.submit();
}
return {
execute: execute,
execute2: execute2,
};
});
Building on the comment from W.S., all you need to do is to define two functions that encapsulate your business logic and then call those functions in order within your scheduled script's entry point.
Below is a simple example, showing the scheduled script executing doFirstTask() followed by doSecondTask(). You can redefine those however you like to meet your own needs.
/**
* #NApiVersion 2.x
* #NScriptType ScheduledScript
*/
define([], function() {
return {
execute: function (context)
{
doFirstTask(context);
doSecondTask(context);
}
}
});
/**
* This function contains all of the logic you need to execute
* for the first of your two tasks.
*
* #param context Passed in from the scheduled script's entry point.
*/
function doFirstTask(context) {
log.debug({
title: context.type + ': First Task',
details: 'The first of many great feats.'
});
}
/**
* This function contains all of the logic you need to execute
* for the second of your two tasks.
*
* #param context Passed in from the scheduled script's entry point.
*/
function doSecondTask(context) {
log.debug({
title: context.type + ': Second Task',
details: 'Second, even more impressive than the first.'
});
}
Here's a screenshot showing what the output looks like.
I'm using this App Script Populate a team vacation calendar
I'm receiving the following error for 1 person in a Google Group of 50 users.
'Error retriving events for email#email.com, vacation:
GoogleJsonResponseException: API call to calendar.events.list failed
with error: Not Found; skipping'
When setting a breakpoint, it fails on this import event.
function importEvent(username, event) {
event.summary = '[' + username + '] ' + event.summary;
event.organizer = {
id: TEAM_CALENDAR_ID,
};
event.attendees = [];
console.log('Importing: %s', event.summary);
try {
Calendar.Events.import(event, TEAM_CALENDAR_ID);
} catch (e) {
console.error('Error attempting to import event: %s. Skipping.',
e.toString());
}
}
do {
params.pageToken = pageToken;
let response;
try {
response = Calendar.Events.list(user.getEmail(), params);
} catch (e) {
console.error('Error retriving events for %s, %s: %s; skipping',
user, keyword, e.toString());
continue;
}
events = events.concat(response.items.filter(function(item) {
return shoudImportEvent(user, keyword, item);
}));
pageToken = response.nextPageToken;
} while (pageToken);
return events;
}
This is the stack:
"GoogleJsonResponseException: API call to calendar.events.list failed
with error: Not Found at findEvents (Code:111:34) at Code:48:20 at
Array.forEach () at Code:47:14 a…"
This is the full code
// Set the ID of the team calendar to add events to. You can find the calendar's
// ID on the settings page.
let TEAM_CALENDAR_ID = 'CALENDAR ID';
// Set the email address of the Google Group that contains everyone in the team.
// Ensure the group has less than 500 members to avoid timeouts.
let GROUP_EMAIL = 'GROUP EMAIL';
let KEYWORDS = ['vacation', 'ooo', 'out of office', 'offline'];
let MONTHS_IN_ADVANCE = 3;
/**
* Sets up the script to run automatically every hour.
*/
function setup() {
let triggers = ScriptApp.getProjectTriggers();
if (triggers.length > 0) {
throw new Error('Triggers are already setup.');
}
ScriptApp.newTrigger('sync').timeBased().everyHours(1).create();
// Runs the first sync immediately.
sync();
}
/**
* Looks through the group members' public calendars and adds any
* 'vacation' or 'out of office' events to the team calendar.
*/
function sync() {
// Defines the calendar event date range to search.
let today = new Date();
let maxDate = new Date();
maxDate.setMonth(maxDate.getMonth() + MONTHS_IN_ADVANCE);
// Determines the time the the script was last run.
let lastRun = PropertiesService.getScriptProperties().getProperty('lastRun');
lastRun = lastRun ? new Date(lastRun) : null;
// Gets the list of users in the Google Group.
let users = GroupsApp.getGroupByEmail(GROUP_EMAIL).getUsers();
// For each user, finds events having one or more of the keywords in the event
// summary in the specified date range. Imports each of those to the team
// calendar.
let count = 0;
users.forEach(function(user) {
let username = user.getEmail().split('#')[0];
KEYWORDS.forEach(function(keyword) {
let events = findEvents(user, keyword, today, maxDate, lastRun);
events.forEach(function(event) {
importEvent(username, event);
count++;
}); // End foreach event.
}); // End foreach keyword.
}); // End foreach user.
PropertiesService.getScriptProperties().setProperty('lastRun', today);
console.log('Imported ' + count + ' events');
}
/**
* Imports the given event from the user's calendar into the shared team
* calendar.
* #param {string} username The team member that is attending the event.
* #param {Calendar.Event} event The event to import.
*/
function importEvent(username, event) {
event.summary = '[' + username + '] ' + event.summary;
event.organizer = {
id: TEAM_CALENDAR_ID,
};
event.attendees = [];
console.log('Importing: %s', event.summary);
try {
Calendar.Events.import(event, TEAM_CALENDAR_ID);
} catch (e) {
console.error('Error attempting to import event: %s. Skipping.',
e.toString());
}
}
/**
* In a given user's calendar, looks for occurrences of the given keyword
* in events within the specified date range and returns any such events
* found.
* #param {Session.User} user The user to retrieve events for.
* #param {string} keyword The keyword to look for.
* #param {Date} start The starting date of the range to examine.
* #param {Date} end The ending date of the range to examine.
* #param {Date} optSince A date indicating the last time this script was run.
* #return {Calendar.Event[]} An array of calendar events.
*/
function findEvents(user, keyword, start, end, optSince) {
let params = {
q: keyword,
timeMin: formatDateAsRFC3339(start),
timeMax: formatDateAsRFC3339(end),
showDeleted: true,
};
if (optSince) {
// This prevents the script from examining events that have not been
// modified since the specified date (that is, the last time the
// script was run).
params.updatedMin = formatDateAsRFC3339(optSince);
}
let pageToken = null;
let events = [];
do {
params.pageToken = pageToken;
let response;
try {
response = Calendar.Events.list(user.getEmail(), params);
} catch (e) {
console.error('Error retriving events for %s, %s: %s; skipping',
user, keyword, e.toString());
continue;
}
events = events.concat(response.items.filter(function(item) {
return shoudImportEvent(user, keyword, item);
}));
pageToken = response.nextPageToken;
} while (pageToken);
return events;
}
/**
* Determines if the given event should be imported into the shared team
* calendar.
* #param {Session.User} user The user that is attending the event.
* #param {string} keyword The keyword being searched for.
* #param {Calendar.Event} event The event being considered.
* #return {boolean} True if the event should be imported.
*/
function shoudImportEvent(user, keyword, event) {
// Filters out events where the keyword did not appear in the summary
// (that is, the keyword appeared in a different field, and are thus
// is not likely to be relevant).
if (event.summary.toLowerCase().indexOf(keyword.toLowerCase) < 0) {
return false;
}
if (!event.organizer || event.organizer.email == user.getEmail()) {
// If the user is the creator of the event, always imports it.
return true;
}
// Only imports events the user has accepted.
if (!event.attendees) return false;
let matching = event.attendees.filter(function(attendee) {
return attendee.self;
});
return matching.length > 0 && matching[0].responseStatus == 'accepted';
}
/**
* Returns an RFC3339 formated date String corresponding to the given
* Date object.
* #param {Date} date a Date.
* #return {string} a formatted date string.
*/
function formatDateAsRFC3339(date) {
return Utilities.formatDate(date, 'UTC', 'yyyy-MM-dd\'T\'HH:mm:ssZ');
}
I was wondering if I can access a specific query parameter from a method from activities.list API
specifically this piece of code
activity.events[0].parameters
I've already tried accessing through array like this:
activity.events[0].parameters[2]
but the output is not consistent, for instance index 2 is not what I need instead its a different query parameter. What I need is for example I need to access
organizer_email
parameter specifically from the API (https://developers.google.com/admin-sdk/reports/v1/appendix/activity/meet)
I'll attach my sample code here. its from google api examples
(https://developers.google.com/admin-sdk/reports/v1/quickstart/apps-script)
/**
* List login events for a G Suite domain.
* #see https://developers.google.com/admin-sdk/reports/reference/rest/v1/activities/list
*/
function getData() {
var today = new Date();
var oneWeekAgo = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000);
var timezone = Session.getScriptTimeZone();
var date = Utilities.formatDate(oneWeekAgo, timezone, 'yyyy-MM-dd');
const userKey = 'all';
const applicationName = 'meet';
var pageToken;
const optionalArgs = {
maxResults: 10,
pageToken: pageToken,
};
try {
const response = AdminReports.Activities.list(userKey, applicationName, optionalArgs);
const activities = response.items;
if (!activities || activities.length === 0) {
Logger.log('No logins found.');
return;
}
// Print login events
Logger.log('MEET LOGS:');
for (const activity of activities) {
Logger.log('(%s) %s %s', activity.events[0].parameters, activity.id.time, activity.id.time);
}
} catch (err) {
// TODO (developer)- Handle exception from the Report API
Logger.log('Failed with error %s', err.message);
}
}
sample array / json result (2 results)
[
{"name":"video_send_seconds","intValue":"40"},
{"name":"screencast_recv_bitrate_kbps_mean","intValue":"128"},
{"value":"email_address","name":"identifier_type"},
{"name":"audio_send_bitrate_kbps_mean","intValue":"2"},
{"name":"video_send_packet_loss_max","intValue":"1"},
{"value":"meetings_android_214151224321356190","name":"endpoint_id"},
{"name":"device_type","value":"android"},{"intValue":"0","name":"video_send_packet_loss_mean"},
{"intValue":"240","name":"video_recv_long_side_median_pixels"},
{"name":"screencast_recv_long_side_median_pixels","intValue":"1072"},
{"name":"screencast_send_seconds","intValue":"0"},
{"name":"video_send_fps_mean","intValue":"15"},
{"intValue":"4","name":"audio_send_packet_loss_max"},
{"intValue":"132","name":"video_recv_short_side_median_pixels"},
{"intValue":"0","name":"video_recv_packet_loss_mean"},
{"name":"screencast_recv_fps_mean","intValue":"8"},
{"intValue":"1267","name":"audio_recv_seconds"},
{"intValue":"0","name":"network_congestion"},
{"name":"network_estimated_download_kbps_mean","intValue":"1398"},
{"intValue":"0","name":"audio_send_packet_loss_mean"},
{"name":"network_transport_protocol","value":"udp"},
{"intValue":"1269","name":"duration_seconds"},
{"intValue":"103","name":"video_send_bitrate_kbps_mean"},
{"name":"identifier","value":"xxxxxx.xxxxx#xxxx.xxx"},
{"intValue":"78","name":"audio_recv_packet_loss_max"},
{"intValue":"10","name":"video_recv_fps_mean"},
{"intValue":"3","name":"audio_recv_packet_loss_mean"},
{"name":"network_recv_jitter_msec_max","intValue":"205"},
{"name":"organizer_email","value":"xxxxxx#xxxxx.xxx"},
{"name":"screencast_recv_short_side_median_pixels","intValue":"480"},
{"name":"network_recv_jitter_msec_mean","intValue":"19"},
{"name":"audio_send_seconds","intValue":"1267"},
{"value":"xxxxxxxxxx","name":"display_name"},
{"name":"screencast_recv_packet_loss_max","intValue":"0"},
{"name":"video_recv_seconds","intValue":"71"},
{"intValue":"35","name":"network_rtt_msec_mean"},
{"name":"video_send_long_side_median_pixels","intValue":"240"},
{"name":"screencast_recv_packet_loss_mean","intValue":"0"},
{"value":"fjqip32o1ru139DFSA2wer23","name":"conference_id"},
{"intValue":"883","name":"screencast_recv_seconds"},
{"name":"product_type","value":"meet"},
{"name":"network_estimated_upload_kbps_mean","intValue":"87"},
{"name":"video_send_short_side_median_pixels","intValue":"132"},
{"intValue":"0","name":"video_recv_packet_loss_max"},
{"name":"meeting_code","value":"ZXWEREHXSG"},
{"name":"is_external","boolValue":true}
]
2nd result:
[
{"name":"video_send_seconds","intValue":"0"},
{"name":"identifier_type","value":"email_address"},
{"name":"audio_send_bitrate_kbps_mean","intValue":"5"},
{"value":"meetings_android_23adjfuhvioalu23xpiow;","name":"endpoint_id"},
{"name":"device_type","value":"android"},
{"intValue":"0","name":"screencast_send_seconds"},
{"intValue":"0","name":"audio_recv_seconds"},
{"intValue":"0","name":"network_congestion"},
{"name":"network_estimated_download_kbps_mean","intValue":"0"},
{"value":"udp","name":"network_transport_protocol"},
{"name":"duration_seconds","intValue":"9"},
{"value":"xxxxxxxxxx.xxxxxxxxx#xxxxxx.xxxxxx","name":"identifier"},
{"value":"xxxxxxxxxxx#xxxxxxxx.xxxxx","name":"organizer_email"},
{"name":"audio_send_seconds","intValue":"7"},
{"name":"display_name","value":"xxxxxxxxxxxxx"},
{"intValue":"0","name":"video_recv_seconds"},
{"intValue":"30","name":"network_rtt_msec_mean"},
{"name":"conference_id","value":"4ersdfgwe3re43edf3s"},
{"name":"screencast_recv_seconds","intValue":"0"},
{"name":"product_type","value":"meet"},
{"name":"network_estimated_upload_kbps_mean","intValue":"0"},
{"name":"meeting_code","value":"43eradsfdas23hgrf"},
{"boolValue":true,"name":"is_external"}
]
found the answer
example usage:
activity.events[0].parameters.find(x => x.name ==='duration_seconds').intValue
I have a script in Google Sheets, which runs a function when a user clicks on an image. The function modifies content in cells and in order to avoid simultaneous modifications I need to use lock for this function.
I cannot get, why this doesn't work (I still can invoke same function several times from different clients):
function placeBidMP1() {
var lock = LockService.getScriptLock();
lock.waitLock(10000)
placeBid('MP1', 'I21:J25');
lock.releaseLock();
}
placeBid() function is below:
function placeBid(lotName, range) {
var firstPrompt = ui.prompt(lotName + '-lot', 'Please enter your name:', ui.ButtonSet.OK);
var firstPromptSelection = firstPrompt.getSelectedButton();
var userName = firstPrompt.getResponseText();
if (firstPromptSelection == ui.Button.OK) {
do {
var secondPrompt = ui.prompt('Increase by', 'Amount (greater than 0): ', ui.ButtonSet.OK_CANCEL);
var secondPromptSelection = secondPrompt.getSelectedButton();
var increaseAmount = parseInt(secondPrompt.getResponseText());
} while (!(secondPromptSelection == ui.Button.CANCEL) && !(/^[0-9]+$/.test(increaseAmount)) && !(secondPromptSelection == ui.Button.CLOSE));
if (secondPromptSelection != ui.Button.CANCEL & secondPromptSelection != ui.Button.CLOSE) {
var finalPrompt = ui.alert("Price for lot will be increased by " + increaseAmount + " CZK. Are you sure?", ui.ButtonSet.YES_NO);
if (finalPrompt == ui.Button.YES) {
var cell = SpreadsheetApp.getActiveSheet().getRange(range);
var currentCellValue = Number(cell.getValue());
cell.setValue(currentCellValue + Number(increaseAmount));
bidsHistorySheet.appendRow([userName, lotName, cell.getValue()]);
SpreadsheetApp.flush();
showPriceIsIncreased();
} else {showCancelled();}
} else {showCancelled();}
} else {showCancelled();}
}
I have several placeBidMP() functions for different elements on the Sheet and need to lock only separate function from being invoked multiple times.
I've tried as well next way:
if (lock.waitLock(10000)) {
placeBidMP1(...);
}
else {
showCancelled();
}
and in this case, it shows cancellation pop-up straight away.
I still can invoke the same function several times from different clients
The documentation is clear on that part: prompt() method won't persist LockService locks as it suspends script execution awaiting user interaction:
The script resumes after the user dismisses the dialog, but Jdbc connections and LockService locks don't persist across the suspension
and in this case, it shows cancellation pop-up straight away
Nothing strange here as well - if statement evaluates what's inside the condition and coerces the result to Boolean. Take a look at the waitLock() method signature - it returns void, which is a falsy value. You essentially created this: if(false) and this is why showCancelled() fires straight away.
Workaround
You could work around that limitation by emulating what Lock class does. Be aware that this approach is not meant to replace the service, and there are limitations too, specifically:
PropertiesService has quota on reads / writes. A generous one, but you might want to set toSleep interval to higher values to avoid burning through your quota at the expense of precision.
Do not replace the Lock class with this custom implementation - V8 does not put your code in a special context, so the services are directly exposed and can be overridden.
function PropertyLock() {
const toSleep = 10;
let timeoutIn = 0, gotLock = false;
const store = PropertiesService.getScriptProperties();
/**
* #returns {boolean}
*/
this.hasLock = function () {
return gotLock;
};
/**
* #param {number} timeoutInMillis
* #returns {boolean}
*/
this.tryLock = function (timeoutInMillis) {
//emulates "no effect if the lock has already been acquired"
if (this.gotLock) {
return true;
}
timeoutIn === 0 && (timeoutIn = timeoutInMillis);
const stored = store.getProperty("locked");
const isLocked = stored ? JSON.parse(stored) : false;
const canWait = timeoutIn > 0;
if (isLocked && canWait) {
Utilities.sleep(toSleep);
timeoutIn -= toSleep;
return timeoutIn > 0 ?
this.tryLock(timeoutInMillis) :
false;
}
if (!canWait) {
return false;
}
store.setProperty("locked", true);
gotLock = true;
return true;
};
/**
* #returns {void}
*/
this.releaseLock = function () {
store.setProperty("locked", false);
gotLock = false;
};
/**
* #param {number} timeoutInMillis
* #returns {boolean}
*
* #throws {Error}
*/
this.waitLock = function (timeoutInMillis) {
const hasLock = this.tryLock(timeoutInMillis);
if (!hasLock) {
throw new Error("Could not obtain lock");
}
return hasLock;
};
}
Version 2
What follows below is closer to the original and solves one important issue with using PropertiesService as a workaround: if there is an unhandled exception during the execution of the function that acquires the lock, the version above will get the lock stuck indefinitely (can be solved by removing the corresponding script property).
The version below (or as a gist) uses a self-removing time-based trigger set to fire after the current maximum execution time of a script is exceeded (30 minutes) and can be configured to a lower value should one wish to clean up earlier:
var PropertyLock = (() => {
let locked = false;
let timeout = 0;
const store = PropertiesService.getScriptProperties();
const propertyName = "locked";
const triggerName = "PropertyLock.releaseLock";
const toSleep = 10;
const currentGSuiteRuntimeLimit = 30 * 60 * 1e3;
const lock = function () { };
/**
* #returns {boolean}
*/
lock.hasLock = function () {
return locked;
};
/**
* #param {number} timeoutInMillis
* #returns {boolean}
*/
lock.tryLock = function (timeoutInMillis) {
//emulates "no effect if the lock has already been acquired"
if (locked) {
return true;
}
timeout === 0 && (timeout = timeoutInMillis);
const stored = store.getProperty(propertyName);
const isLocked = stored ? JSON.parse(stored) : false;
const canWait = timeout > 0;
if (isLocked && canWait) {
Utilities.sleep(toSleep);
timeout -= toSleep;
return timeout > 0 ?
PropertyLock.tryLock(timeoutInMillis) :
false;
}
if (!canWait) {
return false;
}
try {
store.setProperty(propertyName, true);
ScriptApp.newTrigger(triggerName).timeBased()
.after(currentGSuiteRuntimeLimit).create();
console.log("created trigger");
locked = true;
return locked;
}
catch (error) {
console.error(error);
return false;
}
};
/**
* #returns {void}
*/
lock.releaseLock = function () {
try {
locked = false;
store.setProperty(propertyName, locked);
const trigger = ScriptApp
.getProjectTriggers()
.find(n => n.getHandlerFunction() === triggerName);
console.log({ trigger });
trigger && ScriptApp.deleteTrigger(trigger);
}
catch (error) {
console.error(error);
}
};
/**
* #param {number} timeoutInMillis
* #returns {boolean}
*
* #throws {Error}
*/
lock.waitLock = function (timeoutInMillis) {
const hasLock = PropertyLock.tryLock(timeoutInMillis);
if (!hasLock) {
throw new Error("Could not obtain lock");
}
return hasLock;
};
return lock;
})();
var PropertyLockService = (() => {
const init = function () { };
/**
* #returns {PropertyLock}
*/
init.getScriptLock = function () {
return PropertyLock;
};
return init;
})();
Note that the second version uses static methods and, just as LockService, should not be instantiated (you could go for a class and static methods to enforce this).
References
waitLock() method reference
prompt() method reference
Falsiness concept in JavaScript
I have a script in Google Sheets, which runs a function when a user clicks on an image. The function modifies content in cells and in order to avoid simultaneous modifications I need to use lock for this function.
I cannot get, why this doesn't work (I still can invoke same function several times from different clients):
function placeBidMP1() {
var lock = LockService.getScriptLock();
lock.waitLock(10000)
placeBid('MP1', 'I21:J25');
lock.releaseLock();
}
placeBid() function is below:
function placeBid(lotName, range) {
var firstPrompt = ui.prompt(lotName + '-lot', 'Please enter your name:', ui.ButtonSet.OK);
var firstPromptSelection = firstPrompt.getSelectedButton();
var userName = firstPrompt.getResponseText();
if (firstPromptSelection == ui.Button.OK) {
do {
var secondPrompt = ui.prompt('Increase by', 'Amount (greater than 0): ', ui.ButtonSet.OK_CANCEL);
var secondPromptSelection = secondPrompt.getSelectedButton();
var increaseAmount = parseInt(secondPrompt.getResponseText());
} while (!(secondPromptSelection == ui.Button.CANCEL) && !(/^[0-9]+$/.test(increaseAmount)) && !(secondPromptSelection == ui.Button.CLOSE));
if (secondPromptSelection != ui.Button.CANCEL & secondPromptSelection != ui.Button.CLOSE) {
var finalPrompt = ui.alert("Price for lot will be increased by " + increaseAmount + " CZK. Are you sure?", ui.ButtonSet.YES_NO);
if (finalPrompt == ui.Button.YES) {
var cell = SpreadsheetApp.getActiveSheet().getRange(range);
var currentCellValue = Number(cell.getValue());
cell.setValue(currentCellValue + Number(increaseAmount));
bidsHistorySheet.appendRow([userName, lotName, cell.getValue()]);
SpreadsheetApp.flush();
showPriceIsIncreased();
} else {showCancelled();}
} else {showCancelled();}
} else {showCancelled();}
}
I have several placeBidMP() functions for different elements on the Sheet and need to lock only separate function from being invoked multiple times.
I've tried as well next way:
if (lock.waitLock(10000)) {
placeBidMP1(...);
}
else {
showCancelled();
}
and in this case, it shows cancellation pop-up straight away.
I still can invoke the same function several times from different clients
The documentation is clear on that part: prompt() method won't persist LockService locks as it suspends script execution awaiting user interaction:
The script resumes after the user dismisses the dialog, but Jdbc connections and LockService locks don't persist across the suspension
and in this case, it shows cancellation pop-up straight away
Nothing strange here as well - if statement evaluates what's inside the condition and coerces the result to Boolean. Take a look at the waitLock() method signature - it returns void, which is a falsy value. You essentially created this: if(false) and this is why showCancelled() fires straight away.
Workaround
You could work around that limitation by emulating what Lock class does. Be aware that this approach is not meant to replace the service, and there are limitations too, specifically:
PropertiesService has quota on reads / writes. A generous one, but you might want to set toSleep interval to higher values to avoid burning through your quota at the expense of precision.
Do not replace the Lock class with this custom implementation - V8 does not put your code in a special context, so the services are directly exposed and can be overridden.
function PropertyLock() {
const toSleep = 10;
let timeoutIn = 0, gotLock = false;
const store = PropertiesService.getScriptProperties();
/**
* #returns {boolean}
*/
this.hasLock = function () {
return gotLock;
};
/**
* #param {number} timeoutInMillis
* #returns {boolean}
*/
this.tryLock = function (timeoutInMillis) {
//emulates "no effect if the lock has already been acquired"
if (this.gotLock) {
return true;
}
timeoutIn === 0 && (timeoutIn = timeoutInMillis);
const stored = store.getProperty("locked");
const isLocked = stored ? JSON.parse(stored) : false;
const canWait = timeoutIn > 0;
if (isLocked && canWait) {
Utilities.sleep(toSleep);
timeoutIn -= toSleep;
return timeoutIn > 0 ?
this.tryLock(timeoutInMillis) :
false;
}
if (!canWait) {
return false;
}
store.setProperty("locked", true);
gotLock = true;
return true;
};
/**
* #returns {void}
*/
this.releaseLock = function () {
store.setProperty("locked", false);
gotLock = false;
};
/**
* #param {number} timeoutInMillis
* #returns {boolean}
*
* #throws {Error}
*/
this.waitLock = function (timeoutInMillis) {
const hasLock = this.tryLock(timeoutInMillis);
if (!hasLock) {
throw new Error("Could not obtain lock");
}
return hasLock;
};
}
Version 2
What follows below is closer to the original and solves one important issue with using PropertiesService as a workaround: if there is an unhandled exception during the execution of the function that acquires the lock, the version above will get the lock stuck indefinitely (can be solved by removing the corresponding script property).
The version below (or as a gist) uses a self-removing time-based trigger set to fire after the current maximum execution time of a script is exceeded (30 minutes) and can be configured to a lower value should one wish to clean up earlier:
var PropertyLock = (() => {
let locked = false;
let timeout = 0;
const store = PropertiesService.getScriptProperties();
const propertyName = "locked";
const triggerName = "PropertyLock.releaseLock";
const toSleep = 10;
const currentGSuiteRuntimeLimit = 30 * 60 * 1e3;
const lock = function () { };
/**
* #returns {boolean}
*/
lock.hasLock = function () {
return locked;
};
/**
* #param {number} timeoutInMillis
* #returns {boolean}
*/
lock.tryLock = function (timeoutInMillis) {
//emulates "no effect if the lock has already been acquired"
if (locked) {
return true;
}
timeout === 0 && (timeout = timeoutInMillis);
const stored = store.getProperty(propertyName);
const isLocked = stored ? JSON.parse(stored) : false;
const canWait = timeout > 0;
if (isLocked && canWait) {
Utilities.sleep(toSleep);
timeout -= toSleep;
return timeout > 0 ?
PropertyLock.tryLock(timeoutInMillis) :
false;
}
if (!canWait) {
return false;
}
try {
store.setProperty(propertyName, true);
ScriptApp.newTrigger(triggerName).timeBased()
.after(currentGSuiteRuntimeLimit).create();
console.log("created trigger");
locked = true;
return locked;
}
catch (error) {
console.error(error);
return false;
}
};
/**
* #returns {void}
*/
lock.releaseLock = function () {
try {
locked = false;
store.setProperty(propertyName, locked);
const trigger = ScriptApp
.getProjectTriggers()
.find(n => n.getHandlerFunction() === triggerName);
console.log({ trigger });
trigger && ScriptApp.deleteTrigger(trigger);
}
catch (error) {
console.error(error);
}
};
/**
* #param {number} timeoutInMillis
* #returns {boolean}
*
* #throws {Error}
*/
lock.waitLock = function (timeoutInMillis) {
const hasLock = PropertyLock.tryLock(timeoutInMillis);
if (!hasLock) {
throw new Error("Could not obtain lock");
}
return hasLock;
};
return lock;
})();
var PropertyLockService = (() => {
const init = function () { };
/**
* #returns {PropertyLock}
*/
init.getScriptLock = function () {
return PropertyLock;
};
return init;
})();
Note that the second version uses static methods and, just as LockService, should not be instantiated (you could go for a class and static methods to enforce this).
References
waitLock() method reference
prompt() method reference
Falsiness concept in JavaScript