I have a Google spreadsheet that is used concurrently by a team of people located in the Philippines, South America and the U.S. The load on the spreadsheet is usually no more than ten people at a time. The spreadsheet has two sheets, one being a "Submissions" page and the other is named "Archive". There is existing data in the spreadsheet but I don’t believe it’s enough to cause these issues.
The logic is pretty simple. There is a dropdown in Column 14 that has the values "Assignment", "Processing", "Closed", "Duplicate" and "Saved". If the selection is "Assignment" or "Processing" there are timestamps captured at columns 10 and 11. If the selection is "Closed", "Duplicate" or "Saved" there is a timestamp captured on row 12, it checks to make sure the timestamp is captured and the row is moved to the "Archived" tab.
I’m having issues with the script not being consistent capturing the timestamps or moving the row if there are more than one person using the dropdown. Sometimes it works perfectly and captures the timestamps as needed and moves the data as it should. It seems that always if there’s more than one person working in the spreadsheet it has issues.
The other issue is that it’s very slow. It could be because of some of my attempts to make it consistent with locks and retries.
The errors always seem to be either a "Exceeded max execution time" or "Exception: Lock timeout: another process was holding the lock for too long."
I’ve rewritten this and tried different logic and I’m still having issues. My priority is that the script works consistently in capturing timestamps and moving the data, secondly is the speed of the script. My code is below, please overlook any logic that may be incorrect, kind of a newbie with Apps Script.
function onEdit(e) {
if (!e || !e.range) {
Logger.log('Event object is invalid or undefined.');
return;
}
var lock = LockService.getScriptLock();
var maxRetries = 5; // Increased number of retries for high contention
var retryDelay = 10000; // 10 seconds delay between retries
var attempt = 0;
while (attempt < maxRetries) {
try {
// Attempt to acquire the lock.
lock.waitLock(15000); // Wait for up to 15 seconds
var range = e.range;
var sheet = range.getSheet();
var value = range.getValue();
// Only proceed if the edit is in 'Submissions' and in column 14.
if (sheet.getName() !== 'Submissions' || range.getColumn() !== 14) {
return;
}
var timestampColumn = getTimestampColumn(value);
if (timestampColumn === null) {
return;
}
// Check if a timestamp is already present; if so, exit
var existingTimestamp = sheet.getRange(range.getRow(), timestampColumn).getValue();
if (existingTimestamp) {
lock.releaseLock();
return;
}
// Write the timestamp
var timestamp = new Date();
sheet.getRange(range.getRow(), timestampColumn).setValue(timestamp);
// Move row to 'Archived' sheet if needed
if (['Ticket closed', 'Unnecessary', 'Saved'].includes(value)) {
moveRowToCompleted(sheet, range.getRow());
}
break; // Exit loop if successful
} catch (e) {
Logger.log('Error: ' + e.toString());
attempt++;
if (attempt < maxRetries) {
Utilities.sleep(retryDelay); // Wait before retrying
} else {
Logger.log('Exceeded maximum retries. Giving up.');
}
} finally {
lock.releaseLock(); // Ensure the lock is always released
}
}
}
function getTimestampColumn(value) {
switch (value) {
case 'Assignment':
return 10;
case 'Processing':
return 11;
case 'Closed':
case 'Duplicate':
case 'Saved':
return 12;
default:
return null;
}
}
function moveRowToCompleted(sheet, rowIndex) {
var numColumns = sheet.getMaxColumns();
var rowValues = sheet.getRange(rowIndex, 1, 1, numColumns).getValues();
var ss = SpreadsheetApp.getActiveSpreadsheet();
var targetSheet = ss.getSheetByName('Archived');
var targetLastRow = targetSheet.getLastRow();
targetSheet.getRange(targetLastRow + 1, 1, 1, numColumns).setValues(rowValues);
// Delete the row after moving to avoid concurrent modification issues.
sheet.deleteRow(rowIndex);
}
I tried with and without locks and retries. I tried with and without Google’s Api. I am currently trying to clear some data out of the spreadsheet and maybe try to take care of some functionality in the Sheet itself if possible.
Edit: I thought I had installed a trigger? I clicked the plus sign on the "Add Trigger" tab at the bottom right of the page. There was then a prompt with dropdowns for "On Edit"(which function), "Head"(what deployment), "From Spreadsheet"(event Source) and "In Open"(event type). When looking at the "Executions" tab, sometimes the exectutions say "Simple Trigger" others just "Trigger". I may have done something wrong setting it up or trying to use it?
2
Answers
Function name
onEdit is a reserved name for the on-edit simple trigger. If you add an on-edit installable trigger and set onEdit as the trigger’s handler function, the onEdit function might be fired twice by the same edit event.
If you use an on-edit installable trigger, change the name of the onEdit function.
Script performance
In general, calls to Google Apps Script methods are slow, but the
SpreadsheetApp.Sheet.deleterow
and any method that changes the spreadsheet structure is very expensive regarding execution time. You might improve the script performance by using the Advanced Sheet Service, more specifically, theSheets.Spreadsheets.batchUpdate
method.Tips:
SpreadsheetApp.Range.getValue
to get the value of the edited cell, usee.value
. Please remember that this property will returnundefined
when the cell value is cleared.SpreadsheetApp.Range.getColumn
, usee.range.columnStart
. This property is not documented but has been available for a long time.Errors
Exceeded maximum execution time
Simple triggers have a 30-second execution time limit. Installable triggers might have 6 or 30 minutes as the execution time limit, based on the type of account used. For details, see https://developers.google.com/apps-script/quotas
Note: At this time, the quotas page mentions 6 minutes for both types of accounts, but in practice, Workspace accounts, most of the time, have a 30-minute time limit.
Exception: Lock timeout: another process was holding the lock for too long.
This error is caused by the Lock Service script and the users’ behavior. The user’s behavior might be caused by the spreadsheet design.
You might have to rethink your spreadsheet design if you can’t change the user behavior and the script control flow.
Reference
As Wicket notes, you should use a simple trigger and delete any installable triggers you may have created, or rename
onEdit(e)
to something likeinstallableOnEdit(e)
and create exactly one installable trigger to run it. Simple triggers may only run for 30 seconds, while installable triggers may run up to ten times longer. But the root of the problem is probably not with trigger limited runtime — the problem is with the exceedingly long time you say it takes to get a simple timestamp in place and a row moved.Chances are that the bad performance is caused by the spreadsheet rather than the script. To improve spreadsheet performance, see my optimization tips.
The code unnecessarily calls many
SpreadsheetApp
methods inside the loop, includingRange.getSheet()
,Range.getValue()
,Sheet.getName()
,Range.getColumn()
andRange.getRow()
. Replace these calls with references to the event objecte
and move as many API calls outside the loop as possible. See these onEdit(e) optimization tips.Here’s a blueprint for a version that observes some best practices:
Locks are automatically cleared when the script completes so the
finally()
section can be omitted here.Whether locking is required in the first place seems to only depend on whether the
moveRowToCompleted
function is atomic. If it usesSheet.appendRow()
rather thanSheet.setValues()
, and clears the source row rather than deletes it, chances are that it is atomic and you can do without locking. UsingRange.clearContent()
instead ofSheet.deleteRow()
would probably improve performance in any case. Try this:If that is acceptable, you can remove locking in
onEdit(e)
.