17

I've been digging around, and I'm not able to find references or documentation on how I can use Asynchronous Functions in Google App Script, I found that people mention It's possible, but not mention how...

Could someone point me in the right direction or provide me with an example? Promises, Callbacks, or something, that can help me with this.

I have this function lets call it foo that takes a while to execute (long enough that It could time out an HTTP call).

What I'm trying to do Is to refactor it, in a way that it works like this:

function doPost(e) {
    // parsing and getting values from e
    var returnable = foo(par1, par2, par3);
      return ContentService
             .createTextOutput(JSON.stringify(returnable))
             .setMimeType(ContentService.MimeType.JSON);
}

function foo(par1, par2, par3) {
    var returnable = something(par1, par2, par3); // get the value I need to return;

    // continue in an Async way, or schedule execution for something else
    // and allow the function to continue its flow
    /* async bar(); */

    return returnable;
}

Now I want to realize that bit in foo because It takes to long and I don't want to risk for a time out, also the logic that occurs there it's totally client Independent, so It doesn't matter, I just need the return value, that I'll be getting before.

Also, I think It's worth mentioning that this is deployed in Google Drive as a web app.

It's been long since this, but adding some context, at that moment I wanted to scheduled several things to happen on Google Drive, and It was timing out the execution, so I was looking for a way to safely schedule a job.

Rubén
  • 34,714
  • 9
  • 70
  • 166
ekiim
  • 792
  • 1
  • 11
  • 25
  • if multiple `google.script.run.myFunction()` calls are made from client side HTML, the functions called, and that are running on the server, can run at the same time. Is that the situation that you want, or do you want something different? For example, if some HTML loads in the browser for the first time, and you want to inject more content after the initial load, you can make multiple `google.script.run.myFunction()` calls and have multiple instances of code running on the server, and as each one completes, it handles the return. – Alan Wells Dec 18 '18 at 00:43
  • May be I can Edit, the question, I'm using this during an HTTP Call, A Client Calls the app script `doPost` and It needs to _schedule_ the execution, while returning certain value that you get for right before scheduling the execution. – ekiim Dec 18 '18 at 00:47

6 Answers6

12
  • You want to execute functions by the asynchronous processing using Google Apps Script.
  • You want to run the functions with the asynchronous processing using time trigger.

If my understanding is correct, unfortunately, there are no methods and the official document for directly achieving it. But as a workaround, that can be achieved by using both Google Apps Script API and the fetchAll method which can work by asynchronous processing.

The flow of this workaround is as follows.

  1. Deploy API executable, enable Google Apps Script API.
  2. Using fetchAll, request the endpoint of Google Apps Script API for running function.
    • When several functions are requested once, those work with the asynchronous processing by fetchAll.

Note:

  • I think that Web Apps can be also used instead of Google Apps Script API.
  • In order to simply use this workaround, I have created a GAS library. I think that you can also use it.
  • In this workaround, you can also run the functions with the asynchronous processing using time trigger.

References:

If I misunderstand your question, I'm sorry.

Tanaike
  • 181,128
  • 11
  • 97
  • 165
  • If I'm deploying already my Scripts as a WebApp, this still Apply right? – ekiim Dec 18 '18 at 01:08
  • @ekiim Yes. You can also use this workaround using Web Apps. When you want to use Web Apps, please request the endpoint of Web Apps using fetchApp. But please be careful that there are the limitation of workers. Although I think that that is the same with API executable, please confirm about this under your environment. – Tanaike Dec 18 '18 at 01:33
  • @ekiim Is there anything that I can do for you? If your issue was not resolved, I have to think of the solution. – Tanaike Dec 19 '18 at 22:44
  • I Edited the question, because I'm having trouble with something like that. – ekiim Dec 20 '18 at 00:52
  • @ekiim Thank you for replying. You want to achieve the asynchronous processing using Web Apps. You want to run the function ``foo()`` with the asynchronous processing. When you run ``foo()``, you want to run it by giving the different arguments for each worker. If my understanding is correct, I think that that can be achieved. By the way, the added script is the server side. Can you show me the script of client side? – Tanaike Dec 20 '18 at 01:35
  • There Is no Client Side, It's just a HTTP request from a client outside google, and It sends a Json and gets a Json in response – ekiim Dec 20 '18 at 05:26
  • @ekiim If you want to run the Web Apps script with the the asynchronous processing using HTTP request from outside, it can consider the following 2 patterns. 1. Create the the asynchronous request to Web Apps in the script of client side. 2. Prepare 2 Web Apps, and 1st Web Apps includes ``foo()``, 2nd Web Apps includes the script for executing the asynchronous request to the 1st Web Apps. Then the client script run the 2nd Web Apps by HTTP request. – Tanaike Dec 20 '18 at 05:44
  • I took your solution and made a different implementation of it. See my answer here on this question. – toddmo May 16 '20 at 00:46
10

There is another way to accomplish this.

You can use time-based one-off triggers to run functions asynchronously, they take a bit of time to queue up (30-60 seconds) but it is ideal for slow-running tasks that you want to remove from the main execution of your script.

// Creates a trigger that will run a second later
ScriptApp.newTrigger("myFunction")
  .timeBased()
  .after(1)
  .create();

There is handy script that I put together called Async.gs that will help remove the boilerplate out of this technique. You can even use it to pass arguments via the CacheService.

Here is the link:

https://gist.github.com/sdesalas/2972f8647897d5481fd8e01f03122805

// Define async function
function runSlowTask(user_id, is_active) {
  console.log('runSlowTask()', { user_id: user_id, is_active: is_active });
  Utilities.sleep(5000);
  console.log('runSlowTask() - FINISHED!')
}

// Run function asynchronously
Async.call('runSlowTask');

// Run function asynchronously with one argument
Async.call('runSlowTask', 51291);

// Run function asynchronously with multiple argument
Async.call('runSlowTask', 51291, true);

// Run function asynchronously with an array of arguments
Async.apply('runSlowTask', [51291, true]);

// Run function in library asynchronously with one argument
Async.call('MyLibrary.runSlowTask', 51291);

// Run function in library asynchronously with an array of arguments
Async.apply('MyLibrary.runSlowTask', [51291, true]);

Just a word of warning. Google Apps Scripts limits the number of triggers per project so its generally a bad idea to call an async task on a loop.

In other words. Please dont do this:

for (var employeeId = 0; i<100000; i++) {
  Async.call('migrateEmployeeData', employeeId);
}

Instead just move the loop inside your asynchronous task.

// Define async task
function migrateAllEmployees() {
  for (var employeeId = 0; i<100000; i++) {
    // .. do the thing ..
  }
}

// Run async task
Async.call('migrateAllEmployees');
Steven de Salas
  • 20,944
  • 9
  • 74
  • 82
  • 2
    First off, this is a great library to include in your project, could not be easier to use. I wanted to raise an issue that I've recently been seeing an increase in periodic errors `Exception: Clock events must be scheduled at least 1 hour(s) apart.` coming from executing this library. What is perplexing is that it is not consistent, it only seems to happen randomly. Has anyone else seen this issue? – discoStew Oct 21 '22 at 13:06
  • Will this solve Exceeded maximum execution time issue? – olawalejuwonm Jul 26 '23 at 10:18
7

With the new V8 runtime, it is now possible to write async functions and use promises in your app script.

Even triggers can be declared async! For example (typescript):

async function onOpen(e: GoogleAppsScript.Events.SheetsOnOpen) {
    console.log("I am inside a promise");
    // do your await stuff here or make more async calls
}

To start using the new runtime, just follow this guide. In short, it all boils down to adding the following line to your appsscript.json file:

{
  ...
  "runtimeVersion": "V8"
}
smac89
  • 39,374
  • 15
  • 132
  • 179
  • 2
    You can declared them asynchronous.. but they still execute syncronously so this wont really solve the problem.. Gotta love App Scripts. See my answer in https://stackoverflow.com/questions/31241396/is-google-apps-script-synchronous#answer-60174689 – Steven de Salas Mar 25 '21 at 12:34
0

Based on Tanaike's answer, I created another version of it. My goals were:

  • Easy to maintain
  • Easy to call (simple call convention)

tasks.gs

class TasksNamespace {
  constructor() {
    this.webAppDevUrl = 'https://script.google.com/macros/s/<your web app's dev id>/dev';
    this.accessToken = ScriptApp.getOAuthToken();
  }

  // send all requests
  all(requests) {
    return requests
    .map(r => ({
      muteHttpExceptions: true,
      url: this.webAppDevUrl,
      method: 'POST',
      contentType: 'application/json',
      payload: {
        functionName: r.first(),
        arguments: r.removeFirst()
      }.toJson(),
      headers: {
        Authorization: 'Bearer ' + this.accessToken
      }
    }), this)
    .fetchAll()
    .map(r => r.getContentText().toObject())
  }

  // send all responses
  process(request) {
    return ContentService
    .createTextOutput(
      request
      .postData
      .contents
      .toObject()
      .using(This => ({
        ...This,
        result: (() => {
          try {
            return eval(This.functionName).apply(eval(This.functionName.splitOffLast()), This.arguments) // this could cause an error
          }
          catch(error) {
            return error;
          }
        })()
      }))
      .toJson()
    )
    .setMimeType(ContentService.MimeType.JSON)
  }
}

helpers.gs

  // array prototype

  Array.prototype.fetchAll = function() {
    return UrlFetchApp.fetchAll(this);
  }

  Array.prototype.first = function() {
    return this[0];
  }

  Array.prototype.removeFirst = function() {
    this.shift();
    return this;
  }

  Array.prototype.removeLast = function() {
    this.pop();
    return this;
  }


  // string prototype

  String.prototype.blankToUndefined = function(search) {
    return this.isBlank() ? undefined : this;
  };    

  String.prototype.isBlank = function() {
    return this.trim().length == 0;
  }

  String.prototype.splitOffLast = function(delimiter = '.') {
    return this.split(delimiter).removeLast().join(delimiter).blankToUndefined();
  }

  // To Object - if string is Json
  String.prototype.toObject = function() {
    if(this.isBlank())
      return {};
    return JSON.parse(this, App.Strings.parseDate);
  }

  // object prototype

  Object.prototype.toJson = function() {
    return JSON.stringify(this);
  }

  Object.prototype.using = function(func) {
    return func.call(this, this);
  }

http.handler.gs

function doPost(request) {
  return new TasksNamespace.process(request);
}

calling convention

Just make arrays with the full function name and the rest are the function's arguments. It will return when everything is done, so it's like Promise.all()

var a =  new TasksNamespace.all([
    ["App.Data.Firebase.Properties.getById",'T006DB4'],
    ["App.Data.External.CISC.Properties.getById",'T00A21F', true, 12],
    ["App.Maps.geoCode",'T022D62', false]
  ])

return preview

[ { functionName: 'App.Data.Firebase.Properties.getById',
    arguments: [ 'T006DB4' ],
    result: 
     { Id: '',
       Listings: [Object],
       Pages: [Object],
       TempId: 'T006DB4',
       Workflow: [Object] } },
...
]

Notes

  • it can handle any static method, any method off a root object's tree, or any root (global) function.
  • it can handle 0 or more (any number) of arguments of any kind
  • it handles errors by returning the error from any post
toddmo
  • 20,682
  • 14
  • 97
  • 107
  • Great, do you have a repo? – ekiim May 17 '20 at 01:16
  • @ekiim, it's part of a large project which is a private repo; however, now I've included all the helper functions and made the code ready to run after copy/paste – toddmo May 17 '20 at 19:20
  • [Do not extend native prototypes!](https://stackoverflow.com/q/14034180/1048572) Especially not with [enumerable properties on `Object.prototype`](https://stackoverflow.com/q/13296340/1048572)! – Bergi Dec 09 '20 at 21:21
0

// First create a trigger which will run after some time 
ScriptApp.newTrigger("createAsyncJob").timeBased().after(6000).create();

/* The trigger will execute and first delete trigger itself using deleteTrigger method and trigger unique id. (Reason: There are limits on trigger which you can create therefore it safe bet to delete it.)
Then it will call the function which you want to execute.
*/
function createAsyncJob(e) {
  deleteTrigger(e.triggerUid);
  createJobsTrigger();
}

/* This function will get all trigger from project and search the specific trigger UID and delete it.
*/
function deleteTrigger(triggerUid) {
  let triggers = ScriptApp.getProjectTriggers();
  triggers.forEach(trigger => {
    if (trigger.getUniqueId() == triggerUid) {
      ScriptApp.deleteTrigger(trigger);
    }
  });
}
0

While this isn't quite an answer to your question, this could lead to an answer if implemented.

I have submitted a feature request to Google to modify the implementation of doGet() and doPost() to instead accept a completion block in the functions' parameters that we would call with our response object, allowing additional slow-running logic to be executed after the response has been "returned".

If you'd like this functionality, please star the issue here: https://issuetracker.google.com/issues/231411987?pli=1

Adam Eisfeld
  • 1,424
  • 2
  • 13
  • 25