Quick Links

Other stuff

Site owners

  • Bruce Mcpherson

Using named locks with Google Apps Scripts

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

At this point I'm not sure how robust this approach is, but my tests have been promising.  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/d/14MSf2Aez-Nqihhw7Crh-7YZWtg6tM5zIhGs9_7yaDFd5oRxm4nMMzome/edit?usp=sharing or by including library MBaYiatjgCSvDcsG6fHIFsyz3TLx7pV4j

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'});
  
  // 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');
  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');
  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

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 help and more information join our forum,follow the blog or follow me on twitter . For more stuff like this see Google Apps Scripts snippets

Comments