1

My Google Apps Script is deployed as web app and can be accessed by any user. Its functionality is to open and change the text in that document.

I send the script a document ID as a query parameters like so:

https://script.google.com/a/macros/s/AKfycbzCP...TnwHUbXxzDM/exec?docId=1_cMN0nuJadBw6QVjKtdEA6eXhE8ubIoxIJai1ticxnE`

Web app opens the document and changes the text in the document.

function doGet(e){
  var params=e.parameters;
  var doc = DocumentApp.openById(params['docId']);
  ...
  /* change text of the document */
}

Problem

Now when there are more than one user trying to run the app-script simultaneously on the same document, the web app fails to handle concurrency and functionality breaks.

I looked into Lock Service but lock service's document lock only works for container bound scripts and not for web apps.

Then I tried to set property using cache service var cache = CacheService.getDocumentCache(); and property service var documentProperties = PropertiesService.getDocumentProperties(); for the document but document property and document cache returns null in web apps and are restricted to container bound scripts only, as stated in the documentation:

If this method is called outside of the context of a containing document (such as from a standalone script or web app), this method returns null.

Is there any way to handle concurrency for script execution in a document when Google Apps Script is deployed as web app. (Not container bound)

Mogsdad
  • 44,709
  • 21
  • 151
  • 275
m5khan
  • 2,667
  • 1
  • 27
  • 37
  • You were on the right track with Lock Service - you just need to use [Script Lock](https://developers.google.com/apps-script/reference/lock/lock-service#getScriptLock()). – azawaza Jan 19 '16 at 13:56
  • But if I will use script Lock, what will happen is if two different users are working on two different documents independently, and they both will be calling same web app, one user will lock the script and the other one will have to wait for no reason – m5khan Jan 19 '16 at 14:57

1 Answers1

3

As @azawaza points out, you should be using a Lock with an appropriate scope, and a Script Lock is the better fit for your scenario. This is discussed in Does this code lock the onFormSubmit(e) method correctly?

If the critical section of code is sufficiently quick, then there's no real concern about making the user updating Document 2 wait while another update to Document 1 proceeds; they won't wait long. Something like:

function doGet1(e){
  // Perform any "pre" operations on private
  // or non-critical shared resources.
  var params=e.parameters;

  // Get a script lock, because we're about to modify a shared resource.
  var lock = LockService.getScriptLock();
  // Wait for up to 10 seconds for other processes to finish.
  lock.waitLock(10000);

  ////// Critical section begins   vvvvv

  var doc = DocumentApp.openById(params['docId']);

  // change text of the document here

  doc.saveAndClose();

  ////// Critical section ends     ^^^^^
  lock.releaseLock();

  // Continue with operations on private
  // or non-critical shared resources.

  return ContentService.createTextOutput("Document updated.")
}

Specific resource locks

Out of the box, the Google Apps Script Lock Service is designed to protect Critical Sections of code. If we want to control access to a specific resource (perhaps for a long-ish time), such as a Google Document, we can adapt it by changing what we are "locking".

In this example, the Lock service protects a critical section wherein Script Properties are checked and updated. These properties have "keys" that match our docId parameter; the value is not important, as we can use simple existence of the key as our test.

Note: Currently, this script could block a user "forever" (until script times out) if another script fails to delete the property protecting their use of the shared document. You'd want to take greater care in production code.

function doGet2(e){
  // Perform any "pre" operations on private
  // or non-critical shared resources.
  var params=e.parameters;

  // Wait for exclusive access to docId
  var ready = false;
  // Get a script lock, because we're about to modify a shared resource.
  var lock = LockService.getScriptLock();

  while (!ready) {
    // Wait for up to 1 second for other processes to finish.
    if (lock.tryLock(1000)) {
      ////// Critical section begins   vvvvv      

      var properties = PropertiesService.getScriptProperties();

      // If nobody has "locked" this document, lock it; we're ready.
      if (properties.getProperty(docId) == null) {
        // Set a property with key=docId.
        properties.setProperty(docId,"Locked"); 
        ready = true;
      }

      ////// Critical section ends     ^^^^^
      lock.releaseLock();
    }
  }

  // We have exclusive access to docId now.

  var doc = DocumentApp.openById(params['docId']);

  // change text of the document here

  doc.saveAndClose();

  // Delete the "key" for this document, so others can access it.
  properties.deleteProperty(docId); 

  return ContentService.createTextOutput("Document updated.")
}

Named Locks

The logic that we've used in the previous example can be encapsulated into an Object to provide a more elegant interface. In fact, Bruce McPherson has done just that with his cNamedLock Library, described on his Desktop Liberation site. Using that library, you can implement document-specific locking like this:

function doGet3(e){
  // Perform any "pre" operations on private
  // or non-critical shared resources.
  var params=e.parameters;

  // Get a named lock.
  var namedLock = new NamedLock().setKey(docId);

  namedLock.lock();
  ////// Critical section begins   vvvvv      

  // We have exclusive access to docId now.

  var doc = DocumentApp.openById(params['docId']);

  // change text of the document here

  doc.saveAndClose();

  ////// Critical section ends     ^^^^^
  namedLock.unlock();

  return ContentService.createTextOutput("Document updated.")
}
MCL
  • 3,985
  • 3
  • 27
  • 39
Mogsdad
  • 44,709
  • 21
  • 151
  • 275
  • As my core process takes around 2 to 3 seconds for each cycle so even if 10 people will access web app concurrently, wait time will be upto 20 to 30 seconds for the last user in queue. For that "Specific resource locks" seems promising in which I will add the `doc-Id` in script property and will only let those users run the code if their `doc-id` is not in `script properties` but what if it will consume properties read/write quota completely https://developers.google.com/apps-script/guides/services/quotas?hl=en . Should I use cache service instead? – m5khan Jan 20 '16 at 07:04
  • Properties and Cache service are meant to serve different purposes. The expiry of a cache would be a disaster for resource locking, so the PropertiesService is the appropriate choice. There is no read/write quota for Properties, only size limits - and if you are approaching those then Google Apps is the wrong platform for your application. – Mogsdad Jan 20 '16 at 14:30
  • Thanks a lot @Mogsdad, your discerning comments are really helpful. – m5khan Jan 20 '16 at 17:22
  • Not sure whether this was introduced later, but meanwhile there is actually a "Properties read/write" quota of 50,000 / day for "consumers". IMO, the risk introduced with using cache instead of properties seems negligible: This is true at least if the lifetime of a cache entry is really [10 minutes](https://developers.google.com/apps-script/reference/cache/cache#put(String,String)) as this would exceed the maximum script runtime anyway (currently 6 min). Needless to say, properties should always be preferred whenever possible. – MCL Apr 14 '20 at 07:51