Service Invoked Too Many Times (Google Apps Script) - google-apps-script

I want to use Google Apps Script to make custom functions for a spreadsheet. I've made an extremely simple function:
function foo(){
return "bar";
};
The problem is that I need this function in a couple hundred cells. When I paste the function =foo() into all of these cells, the function works in a few of the cells, but in most I get this error: "Service invoked too many times: spreadsheet. Try Utilities.sleep(1000) between calls."
[Screenshot here]
I guess I don't understand why this function, simple as it is, is considered an invocation of the Spreadsheet Services. I'm not even requesting any data (except for the function itself). Is that the problem? And if so, is there a workaround? Custom functions could make Google Spreadsheets infinitely more powerful, but this problem hamstrings the possibility of using a custom function in multiple cells. Suggestions?
(P.S. -- Using the Utilities.sleep() function as suggested by the error message doesn't help at all when all of the cells call their functions simultaneously; it only slows the rate at which individual cells repeatedly call the function.)

According to the Optimization section on the Apps Script Function Guide:
Each time a custom function is used in a spreadsheet, Google Sheets makes a separate call to the Apps Script server. If your spreadsheet contains dozens (or hundreds, or thousands!) of custom function calls, this process can be quite slow.
Consequently, if you plan to use a custom function multiple times on a large range of data, consider modifying the function so that it accepts a range as input in the form of a two-dimensional array, then returns a two-dimensional array that can overflow into the appropriate cells.
To do this, pass in an input that represents the size of the array you'd like to return. When you start executing your function check if the input parameter is an array with input.map. If it is, you can call the the function on each item and return that entire collection.
So in your case like this:
function foo(){
return "bar";
};
You can update the function like this:
function foo(input){
if (input.map) { // Test whether input is an array.
return input.map(foo); // Recurse over array if so.
} else {
// do actual function work here
return "bar";
}
};
And then call it like this:

By calling the function in the spreadsheet, you are invoking the Spreadsheet service by asking it to go round-trip to the server to run the results of your function. As a result, you have make a couple hundred requests in a very short period of time.
One work around might be to add your function a few cells at a time. Of course, when you subsequently open the sheet again, you will probably run into the same problem.
Depending on what your function is trying to accomplish, it might be worth using the built in spreadsheet functions. There is a lot of power there. Writing a function that acts on a range of values instead of a single cell might be another, better, option. It could be triggered through a custom menu item, or by using the script manager.
Keep in mind, batch actions are your best friend when it comes to working with spreadsheets.

Related

Looking for an onEdit script that can be used to change the color of a google sheets cell when it is edited?

I am using google sheets to monitor a project that includes multiple users. I would like to use an onEdit command to change a cell's color to yellow when the content is edited. Below is the code that I am working with. However, I am unable to get it to function. Coding is still new to me, so any advice would be greatly appreciated.
function onEdit1(e)
{
var range=e.range;
var column=range.getColumn();
if(column>3 && column<27)
{
range.setBackground('#ffff00')'
}
}
function onEdit(e){
if(e.range.getSheet().getName()!="Sheet1")return;
if(e.range.columnStart>3 && e.range.columnStart<27) {
e.range.setBackground('#ffff00');
}
}
I also added a line to limit it to only one sheet. You may wish to change the name of that sheet or even remove it entirely if you want it to run on your entire spreadsheet.
A lot of new programmers try to run these onEdit(e) functions from the script editor. Unfortunately, that doesn't work because the e parameter is expecting to be populated by the event trigger. Without the event object you'll normally get an error like Cannot read property range from undefined because e has not been populated by the event trigger.
I test them by making sure I'm editing the correct sheet and correct range and I use the e.source.toast() function to provide me with feed back sort of like the console.log() does.
If you want to learn more about the event object then try adding a Logger.log(JSON.stringify(e)); to the first line after the function declaration. And then get it to run by editing the appropriate sheet in the appropriate way and go to view log to see the results.

Try Catch for if script is already running in sheets, prevent a 2nd instance at same time?

I have a script that does some basic copy and pasting of data, creates a sheet as a pdf and attaches to an email.I have a button to start it for my less-tech-experienced colleagues embedded in a sheet:
We share the spreadsheet doc and I'd like to keep it that way for version control. What's the best way to set the script up so that if an instance of the script is already running in the spreadsheet doc it rejects a 2nd attempt until the first one is completed? Some sort of try catch?
I don't think the code is as relevant for this question, here's the beginning of the function in question:
function failedSettlementsEmailz() {
var contacts;
var toastMembers="";
....more code...
It calls two other functions in my script document that is bound to the Google Sheet spreadsheet.
Thanks
Take a look at the LockService.
For example in your code have:
var lock = LockService.getScriptLock();
if (lock.tryLock(1000)) { // Wait for 1s
// Do stuff ...
lock.releaseLock()
} else {
// This script is already running, try again later ...
}
I understood that when the script is run by clicking a button, you don't want to run the script more while the script is already running. If my understanding is correct, how about this workaround? Please think of this as one of several answers.
In this workaround, I selected to use CacheService. "CacheService allows you to access a cache for short term storage of data." The default expiration is 10 minutes. This is over the maximum execution time of GAS (6 minutes). So I selected this.
Flow :
Retrieve the key of "script" from CacheService.
If there is the key, it means that the script is running.
If there is no key, it means that the script is not running.
At this time, put the key of "script" with a value.
The process you want is run.
After the process was finished, the key is removed. This means the script is not running.
Sample script :
function failedSettlementsEmailz(){
var cache = CacheService.getScriptCache();
if (!cache.get("script")) {
cache.put("script", "running"); // you can use various value for the value of "running".
// do something
cache.remove("script");
}
}
Reference :
CacheService
If I misunderstand your question, I'm sorry.

Autocomplete Not Working - Google App Script

I'm having trouble with the autocomplete feature in Google App Script.
Built-in methods like SpreadsheetApp. will provide an autocomplete menu with methods to choose from.
However, if I create my own child object, autocomplete works for a little while, and then it just stops working.
for example:
var skywardRoster = SpreadsheetApp.getActiveSheet();
skywardRoster. will produce method options for a while, and then it stops.
However, the code still functions, and methods work if I type them out manually, so I know the declarations must be right. The menu simply won't appear, and it's just very inconvenient to have to look up each method individually as I go.
I have tried: breaking the variable and retyping that line; copy and pasting the code back into the editor; using different browsers; copying the gs file itself and working within the copy; and signing out completely and signing back in. Nothing seems to get it back to work.
I'm really new to coding, and I'm not sure what can be causing this.
Does anyone know how to fix this issue?
You might want to check Built-in Google Services:Using autocomplete:
The script editor provides a "content assist" feature, more commonly called "autocomplete," which reveals the global objects as well as methods and enums that are valid in the script's current context. To show autocomplete suggestions, select the menu item Edit > Content assist or press Ctrl+Space. Autocomplete suggestions also appear automatically whenever you type a period after a global object, enum, or method call that returns an Apps Script class. For example:
If you click on a blank line in the script editor and activate autocomplete, you will see a list of the global objects.
If you type the full name of a global object or select one from autocomplete, then type . (a period), you will see all methods and enums for that class.
If you type a few characters and activate autocomplete, you will see all valid suggestions that begin with those characters.
Since this was the first result on google for a non-working google script autocompletion, I will post my solution here as it maybe helps someone in the future.
The autocompletion stopped working for me when I assigned a value to a variable for a second time.
Example:
var cell = tableRow.appendTableCell();
...
cell = tableRow.appendTableCell();
So maybe create a new variable for the second assignment just during the implementation so that autocompletion works correctly. When you are done with the implementation you can replace it with the original variable.
Example:
var cell = tableRow.appendTableCell();
...
var replaceMeCell = tableRow.appendTableCell(); // new variable during the implementation
And when the implementation is done:
var cell = tableRow.appendTableCell();
...
cell = tableRow.appendTableCell(); // replace the newly created variable with the original one when you are done
Hope this helps!
I was looking for a way how to improve Google Apps Script development experience. Sometimes autocomplete misses context. For example for Google Spreadsheet trigger event parameters. I solved the problem by using clasp and #ts-check.
clasp allows to edit sources in VS Code on local machine. It can pull and push Google Apps Script code. Here is an article how to try it.
When you move to VS Code and setup environment you can add //#ts-check in the beginning of the JavaScript file to help autocomplete with the special instructions. Here is the instructions set.
My trigger example looks like this (notice autocompletion works only in VS Code, Google Apps Script cloud editor doesn't understand #ts-check instruction):
//#ts-check
/**
* #param {GoogleAppsScript.Events.SheetsOnEdit} e
*/
function onEditTrigger(e) {
var spreadsheet = e.source;
var activeSheet = spreadsheet.getActiveSheet();
Logger.log(e.value);
}
I agree, Google Script's autocomplete feature is pretty poor comparing with most of other implementations. However the lack is uderstandable in most cases and sometimes the function can be preserved.
Loosing context
The autocomplete is limited to Google objects (Spreasheets, Files, etc.). When working with them you get autocomplete hints, unless you pass such object instance to function as an argument. The context is lost then and the editor will not give you suggestions inside the called function. That is because js doesn't have type control.
You can pass an id into the function instead of the object (not File instance but fileId) and get the instance inside of the function but in most cases such operation will slow the script.
Better solution by Cameron Roberts
Cameron Roberts came with something what could be Goole's intence or a kind of hack, don't know. At the beginning of a function assign an proper object instance to parameter wariable and comment it to block:
function logFileChange(sheet, fileId){
/*
sheet = SpreadsheetApp.getActiveSheet();
*/
sheet.appendRow([fileId]); // auto completion works here
}
Auto completion preserved

Can lockservice for GAS work across multiple functions in the same project

I had a problem with a script I wrote, the solution to the issue was lock service to avoid collisions with form submits. As I had no idea this would be an issue, I've had to go back and revisit old scripts.
I have script that has a few different functions, and it passes data from one function to another. Eventually it writes data to sheet, creates a PDF, can email it and stores the PDF to a folder in google drive.
Here's a brief example of what I mean
function firstFunction() {
//Do stuff, return something
return something;
secondFunction(something);
}
function secondFunction(something) {
// Do stuff, return test
thirdFunction(test);
}
function thirdFunction(test) {
// Do stuff, return that
return that;
fourthFunction(that);
}
function fourthFunction(that){
// finish doing stuff. Write data
}
I also have a separate script, that would invoke the first and iterate through a list of data, to bulk produce PDFs.
I'm worried that if 2 people invoke the script at the same time, I'll have issues again.
Given the example script I've given, Do I have to use LockService on each function? Or can I declare the lock in the first function and release it in the last.
I'm also curious on how it would sit with the 2nd script that invokes the first several times. Would adding the lock service in this one be sufficient, or would I also have to add it to the second too?
Thanks in advance.
EDIT BELOW
I just remembered I posted the real code on Code Review for advice, and boy did I get some!!
Code Review Post
I should think that you don't need Lock Service in this case at all.
In the Lock Service documentation it states:
[Lock Service] Prevents concurrent access to sections of code. This can be useful when you have multiple users or processes modifying a shared resource and want to prevent collisions. (documentation: https://developers.google.com/apps-script/reference/lock/lock-service#getScriptLock%28%29)
or [Class Lock] is particularly useful for callbacks and triggers, where a user action may cause changes to a shared resource and you want to ensure that aren't collisions. (documentation: https://developers.google.com/apps-script/reference/lock/lock#tryLock%28Integer%29)
Now, having read the script code that you link to in your edit, I saw no shared resources that the script is writing to. So I conclude no lock is required. (EDIT: On second reading, I see that the script is writing to a sheet once, the shared resource. So your lock can go within that function only.)
I will cross post this point to Google Apps Script Plus community https://plus.google.com/communities/102471985047225101769 since there are experts there who can confirm.

How to Access ScriptDB from Spreadsheet Custom Function?

In this post, I suggested to use the ScriptDB as an intermediate storage for global data of a Container Extension code. I wrote a sample code for my answer but the sample throws the error: You do not have permission to call query (line X) exception in a ScriptDb.getMyDb().query(...); line. I created the following simpler example demonstrating the problem. The code, both getDBSize and getSource functions, is permitted to use the ScriptDB by running it in the editor. The getDBSize function is executed without any problem by pressing the Run button in the Spreadsheet Script Manager Dialog. The getSource function works everywhere.
I published the Spreadsheet for the example - link. It is impossible to share the code for view, but it is possible to output it in a cell, the cell B3 contains exactly bellow code.
How is possible to permit the Spreadsheet Code to have access to the ScriptDB?
function getDBSize() {
var db = ScriptDb.getMyDb();
var result = db.query({});
var count = result.getSize();
return count;
}
function getSource() {
return this.toSource();
}
The problem is that you're trying to run this function as a spreadsheet custom function, and custom functions are way more limited than all examples on the Container Extension page you linked.
But, from a theoretical point of view, custom functions as well as simple event handlers (e.g. onEdit, onOpen), can not access anything that requires the user account or is associated with the user, only "generic" methods. For example, UrlFetchApp, open the current spreadsheet, or read ScriptProperties.
So, in thesis, querying a ScriptDb should be possible, since it's a generic call and has nothing to do with the active user, it's analogous to ScriptProperties. I don't see a workaround that would actually let you query the db, but you could use ScriptProperties instead. You can easily save and retrieve any object you would save on ScriptDb by using JSON.stringify and .parse. Of course, I'm not comparing ScriptDb capabilites with ScriptProperties, it's just a workaround.
Anyway, this seems like a good candidate for an enhancement request on our issue tracker.