The LockService gives the capability to do this

This service allows scripts to 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.

Very nice, but it’s description is a little vague. At best we can prevent some piece of code (it’s not clear what the scope is exactly), being executed more than once at a time. This is good, but what I found I needed was to be able to put a lock on a specific resource, perhaps that is accessed from multiple places.

Named lock.

The characteristics of a named lock are the same as the LockService

  • Expires after some period of time
  • Can be unlocked on successful completion before expiry

With the important addition

  • can be one of multiple independent locks
  • can apply to an abstract resourced, identified by some name, not to a specific piece of code.

Approach

Note that this technique relies on the predictability of CacheService. Google warn that it’s not necessarily predictable, so at this point I’m not sure how robust this approach is, but I have many people out there using this now and haven’t had any issues reported.

I’m using the library from Database caching, to create a named key in public cache that serves as a key to an in use marker, and of course the LockService itself to prevent collisions while grabbing a piece of cache. Would be great to get your experiences using this library, which can take a copy of at

https://script.google.com/a/mcpher.com/d/Mpv7vUR0126U53sfSMXsAPai_d-phDA33/edit?usp=sharing

or by including library Mpv7vUR0126U53sfSMXsAPai_d-phDA33

or on github

Example

create a named lock

var namedLock = new NamedLock();


set a key

namedLock .setKey('some name');

check to see if there is a lock on that key

if (namedLock.isLocked()) {
    ..do something
	}

get a lock

if ( !namedLock.lock()) {
... failed to get one
}

remove a lock

namedLock.unlock();

get info about a lock

Logger.log( namedLock.getInfo() );

Some more fancy things

adjust the expiry time for a lock (in milliseconds)

var namedLock = new NamedLock(5000);

adjust the time to wait before giving up (in milliseconds)

var namedLock = new NamedLock(5000, 10000);

use a variety of arguments from which to generate a named key

namedLock .setKey('a', 1,{x:'a',y:'b'});

set a lock owner (will be a property of lock.getInfo() and is not part of the key)

if ( !namedLock.lock('my lock')) {
... failed to get one
}

Info contents

{"key":"FdFNBlLXuloKWgcnNW/DzA==","when":1403717785812,"who":"my lock","expires":1403717787812}

Even more fancy

Run the whole section of code in a protected lock anonymous function

var p = new NamedLock().setKey("some shared resource").protect ("me", function () {
    Logger.log ("im protected");
    return 'this function ran';
	});

Some tests

var namedLock = new NamedLock().setKey('a', 1,{x:'a',y:'b'}).setSameInstanceLocked(true);
  
  // unlock from previous attempt
  namedLock.unlock();
  Logger.log('should be false');
  Logger.log(namedLock.isLocked());
[14-06-26 10:18:40:531 BST] should be false
[14-06-26 10:18:40:555 BST] false
  // take a lock
  namedLock.lock();
  Logger.log('should be true');
  Logger.log(namedLock.isLocked());
[14-06-26 10:18:40:705 BST] should be true
[14-06-26 10:18:40:732 BST] true
  //unlock
  namedLock.unlock();
  Logger.log('should be false');
  Logger.log(namedLock.isLocked());
[14-06-26 10:18:40:757 BST] should be false
[14-06-26 10:18:40:782 BST] false
//try some other key
  namedLock.setKey('something else');
  // unlick from previous attempt
  namedLock.unlock();
  Logger.log('should be false');
  Logger.log(namedLock.isLocked());
[14-06-26 10:18:40:833 BST] should be false
[14-06-26 10:18:40:858 BST] false
// try with a small timeout
  var shortLock = new NamedLock(2000).setKey('blub').setSameInstanceLocked(true);
  Logger.log(shortLock.lock() ? 'got a lock':'didnt get a lock');
  Logger.log('should be true');
  Logger.log(shortLock.isLocked());
  var info = shortLock.getInfo();
  Logger.log(JSON.stringify(info));
[14-06-26 10:18:41:057 BST] got a lock
[14-06-26 10:18:41:057 BST] should be true
[14-06-26 10:18:41:092 BST] true
[14-06-26 10:18:41:118 BST] {"key":"FdFNBlLXuloKWgcnNW/DzA==","when":1403774320933,"who":"anonymous","expires":1403774322973}
// sleep and then check again
  Utilities.sleep(2500);
  Logger.log(info.expires < Date.now() ? 'expired':'not expired');
  Logger.log(Date(info.expires).toString());
  Logger.log('should be false');
  Logger.log(shortLock.isLocked());
[14-06-26 10:18:43:629 BST] expired
[14-06-26 10:18:43:630 BST] Thu Jun 26 2014 10:18:43 GMT+0100 (BST)
[14-06-26 10:18:43:630 BST] should be false
[14-06-26 10:18:43:667 BST] false
// try again
  Logger.log(shortLock.lock("test function") ? 'got a lock':'didnt get a lock').setSameInstanceLocked(true);
  var info = shortLock.getInfo();
  Logger.log(info.expires < Date.now() ? 'expired':'not expired');
  Logger.log(Date(info.expires).toString());
  Logger.log(JSON.stringify(info));
  Logger.log('should be true');
  Logger.log(shortLock.isLocked());
[14-06-26 10:18:43:928 BST] got a lock
[14-06-26 10:18:43:953 BST] not expired
[14-06-26 10:18:43:953 BST] Thu Jun 26 2014 10:18:43 GMT+0100 (BST)
[14-06-26 10:18:43:954 BST] {"key":"FdFNBlLXuloKWgcnNW/DzA==","when":1403774323865,"who":"test function","expires":1403774325905}
[14-06-26 10:18:43:954 BST] should be true
[14-06-26 10:18:43:976 BST] true
 var p = new NamedLock().setKey("some shared resource").protect ("me", function () {
    Logger.log ("im protected");
    return 'this function ran';
  });
  Logger.log(JSON.stringify(p));
[14-06-26 10:18:44:122 BST] im protected
[14-06-26 10:18:44:148 BST] {"locked":true,"result":"this function ran"}

Lock instances

There is the concept of a lock ‘instance’. The purpose of this is so that, in the same instance of a script, you can choose whether or not a lock is inherited or independent within the script.

function testSameInstance () {

  var resource = "some resource or other";
  var namedLock = new NamedLock().setKey(resource);
  
  namedLock.unlock();
  namedLock.lock();
  
  // when i check for it being locked, it should be false because we have the same lock instance
  Logger.log("should be false");
  Logger.log(namedLock.isLocked());
  
  // whereas a different instance will be locked
  Logger.log("should be true");
  Logger.log(new NamedLock().setKey(resource).isLocked());

}
[14-06-27 16:32:43:109 BST] should be false
[14-06-27 16:32:43:130 BST] false
[14-06-27 16:32:43:130 BST] should be true
[14-06-27 16:32:43:153 BST] true

This means that you will always get a lock for a particular instance if you already have a lock, but a different instance with the same resource key will show as already being locked. This leaves you free to not worry about calling a ‘lock within a lock’ .

function tryInstance() {

  var resource = 'some resource';
  var globalLock = new NamedLock(20000).setKey(resource);
  
  // clear any previus lock attempt
  globalLock.unlock();
  
  globalLock.lock();
  within();
  
  
  function within () {
 
    Logger.log("should be false");
    Logger.log(globalLock.isLocked());
    
    var localLock = new NamedLock().setKey(resource);
    Logger.log("should be true");
    Logger.log(localLock.isLocked());
  
    // so you can get another lock without caring if some other part of your script has already got one.
    globalLock.lock();
    
    Logger.log("should be false");
    Logger.log(globalLock.isLocked());
  }
  
  
  }
[14-06-27 17:06:52:384 BST] should be false
[14-06-27 17:06:52:406 BST] false
[14-06-27 17:06:52:409 BST] should be true
[14-06-27 17:06:52:432 BST] true
[14-06-27 17:06:52:683 BST] should be false
[14-06-27 17:06:52:704 BST] false

SetSameInstanceLocked

The purpose of instance locking is to enable the nesting of locks for the same instance without then interfering with each other. This means that taking a lock and then testing it with the same instance will return false – since as far as this instance is concerned it’s not locked out.

var globalLock = new NamedLock().setKey(resource);
 Logger.log(globalLock.isLocked());   //// returns false

It is possible to modify this behavior with

.setSameInstanceLocked(true);

in which case a lock on the same instance will lock out other attempts using the same instance

var globalLock = new NamedLock().setKey(resource).setSameInstanceLocked(true);
 Logger.log(globalLock.isLocked());   //// returns true

When a lock is attempted with the same instance, it is allowed immediately, and the expiry time updated

var globalLock = new NamedLock().setKey(resource);
 globalLock.lock();   //// granted immediately

Whereas this would have to wait

var globalLock = new NamedLock().setKey(resource).setSameInstanceLocked(true);
 globalLock.lock();   //// has to wait

A test with multiple triggers

In this example I trigger 10 fairly simultaneous executions of the code below, and protect the properties service where I’m holding the results, sleeping for a bit to provoke a lock collision and check that the properties have not been updated by something else in the meantime. In the results, the delay between started, and got lock, indicates that a wait for a lock to be released was happening while the instance with the lock was sleeping as intended.

function propnTrig() {

  var report= {started:new Date(Date.now()).toString()};
  var namedLock = new NamedLock().setKey('my resource');

  if (!namedLock.protect("me",function (lock) {
    report.gotLock = new Date(Date.now()).toString();
    
    // get accumulated reports
    var p = props.getProperty(key);
    var o = p ? JSON.parse(p) : [];
    
    // show how many we have
    o.push(report);
    report.sequence = o.length;
    
    // store it
    var s = JSON.stringify(o);
    report.storing =  new Date(Date.now()).toString();
    
    props.setProperty(key, s);
    
    // wait a bit to provoke some lock collisions
    Utilities.sleep(10000);
    
    // confirm it hasnt changed
    var t = props.getProperty(key);
    
    if (t!==s) {
      report.messedUp = {expected:s,got:t,at: new Date(Date.now()).toString()};
      props.setProperty(key, JSON.stringify(o));
    }
    
  }).locked) {
      // log error in a different property altogether
    report.failed = new Date(Date.now()).toString();
    errorProps.setProperty(key, report);
  }

}

results

[
    {
        "started": "Fri Jun 27 2014 10:01:07 GMT+0100 (BST)",
        "gotLock": "Fri Jun 27 2014 10:01:08 GMT+0100 (BST)",
        "sequence": 1
    },
    {
        "started": "Fri Jun 27 2014 10:01:11 GMT+0100 (BST)",
        "gotLock": "Fri Jun 27 2014 10:01:19 GMT+0100 (BST)",
        "sequence": 2
    },
    {
        "started": "Fri Jun 27 2014 10:01:16 GMT+0100 (BST)",
        "gotLock": "Fri Jun 27 2014 10:01:30 GMT+0100 (BST)",
        "sequence": 3
    },
    {
        "started": "Fri Jun 27 2014 10:01:17 GMT+0100 (BST)",
        "gotLock": "Fri Jun 27 2014 10:01:41 GMT+0100 (BST)",
        "sequence": 4
    },
    {
        "started": "Fri Jun 27 2014 10:01:33 GMT+0100 (BST)",
        "gotLock": "Fri Jun 27 2014 10:01:51 GMT+0100 (BST)",
        "sequence": 5
    },
    {
        "started": "Fri Jun 27 2014 10:01:13 GMT+0100 (BST)",
        "gotLock": "Fri Jun 27 2014 10:02:02 GMT+0100 (BST)",
        "sequence": 6
    },
    {
        "started": "Fri Jun 27 2014 10:01:53 GMT+0100 (BST)",
        "gotLock": "Fri Jun 27 2014 10:02:13 GMT+0100 (BST)",
        "sequence": 7
    },
    {
        "started": "Fri Jun 27 2014 10:01:52 GMT+0100 (BST)",
        "gotLock": "Fri Jun 27 2014 10:02:24 GMT+0100 (BST)",
        "sequence": 8
    },
    {
        "started": "Fri Jun 27 2014 10:01:58 GMT+0100 (BST)",
        "gotLock": "Fri Jun 27 2014 10:02:35 GMT+0100 (BST)",
        "sequence": 9
    },
    {
        "started": "Fri Jun 27 2014 10:01:51 GMT+0100 (BST)",
        "gotLock": "Fri Jun 27 2014 10:02:46 GMT+0100 (BST)",
        "sequence": 10
    }
	]

Code from library

For more like this see Google Apps Scripts Snippets
For help and more information join our forum,follow the blog or follow me on Twitter.