All about locking


The LockService is sometimes misunderstood. It says this in the documentation:-

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.

Does the lock apply to code fragments or the entire library ?


I've noticed some discussions on Stackoverflow and elsewhere that the lock applies to a library. So let's test that. I have a library that has a couple of functions in it. One is protected by the Lockservice, the other is not. I'll kick off tests more or less at once in the order t1, t2, t3 - where t1 and t2 execute protected code (so I expect them to run interleaved with no overlap between them) and t3 runs unprotected so i expect to see some overlap with both t2 and t3.

Here's what happens - the unprotected code is allowed to run whenever, but the protected code waits to be allocated a lock. Therefore the lock applies not to the library, but to the protected code. 





Is there only one lock per library ?

We've established then, that the lockservice applies to a chunk of code, not to a library. Can we get more than one instance of a lock in a library, and are they independent? Let's run the same test again, but this time, t2 will try to take a different lock instance. If there is only one instance of a lock per library, then we'll get a similar result to before. Otherwise t2 and t1 will be able to overlap.



We can see that as before, the unprotected t3 can run when it likes, but t1 & t2 use the same lock, even though different parts of code are being protected.

Conclusion

There is only one lock per library and all protected code takes the same lock. Unprotected code is not affected by locking activities. This means that if you want to take out locks depending on some variable values (for example locking a specific sheet), you can't use the same library to do it. To get round this, I introduced the concept of named locks. This works by taking a lock on an abstract resource, and using cache or property service to record and manage the locks. In this way you can use the same code to take locks of different resources. 

You can read more about that and get the library in Using named locks with Google Apps Scripts . Watch this space for a more detailed tutorial.


The test code

Here's the tests t1,t2,t3


function myFunction() {
  var handler = locktest.getTheSheet(), p =[];
  
  for (var i=0 ; i < locktest.NUMBER_TESTS ; i++) {
    p.push(locktest.protected('t1-'+i));
  }
  
  handler.save(p);
}

function myFunction() {
  var handler = locktest.getTheSheet(), p =[];
  
  for (var i=0 ; i < locktest.NUMBER_TESTS ; i++) {
    p.push(locktest.protected2('t2-'+i));
  }
  
  handler.save(p);
}

function myFunction() {
  var handler = locktest.getTheSheet(), p =[];
  
  for (var i=0 ; i < locktest.NUMBER_TESTS ; i++) {
    p.push(locktest.unprotected('t3-'+i));
  }
  
  handler.save(p);
}

And here's the common library
function protected (name) {
  var result = {name:'protected_' + name,called:Date.now(),  started:null,finished:null};
  return gasLockProtect_ (name, function () {
    
    // this code is locked
    result.started = Date.now();
    Utilities.sleep(2000+Math.random ()*1000);
    result.finished = Date.now();
    return result;
    
  });
}

function protected2 (name) {
  var result = {name:'protected_' + name,called:Date.now(),  started:null,finished:null};
  return gasLockProtect_ (name, function () {
    
    // this code is locked
    result.started = Date.now();
    Utilities.sleep(2000+Math.random ()*1000);
    result.finished = Date.now();
    return result;
    
  });
}


function unprotected (name) {
    var result = {name:'unprotected_' + name, called:Date.now(),  finished:null,started:null};
    result.started = Date.now();
    Utilities.sleep(2000+Math.random ()*1000);
    result.finished = Date.now();
    return result;
}
  
  
var WAIT_TIME = 10000;
var NUMBER_TESTS = 10;
function gasLockProtect_ (who, f) {
  var gasLock = LockService.getPublicLock();
  gasLock.tryLock(WAIT_TIME);
  
  if (gasLock.hasLock()) {
    
    // do the work
    try {
      var r = f(gasLock);
    }
    catch (err) {
      throw (err);
    }
    finally {
      gasLockRelease_(gasLock,who);
    }
  }
  else {
    throw 'unable to get a gasLock';
  }
  return r;
}


function gasLockRelease_(gasLock,who) {
  // drop the lock
  try {
    if (gasLock) { 
      gasLock.releaseLock();
    }
    else {
      throw 'tried to release invalid gas lock for ' + who + ' ' + JSON.stringify(e);
    }
  }
  catch (e) {
    throw 'gas lock not released for ' + who + ' ' + JSON.stringify(e);
  }
}
  

function getTheSheet() {

  var handler = new cDataHandler.DataHandler (
    'random',                        
    cDataHandler.dhConstants.DB.SHEET,      
    undefined,
    '12pTwh5Wzg0W4ZnGBiUI3yZY8QFoNI8NNx_oCPynjGYY');

  
  assert(handler.isHappy(), 'unable to get sheet handler','handler');
  
  return handler;

}

function assert (what,message,n) {
  var fatal = true;
  if (!Array.isArray(what)) what = [what];
  var good = what.every(function(d) { return d });
  var m = ('assertion' + n + (good ? ':success':':failure:'+JSON.stringify({message:message,tests:what})));
  Logger.log(m);
  if (!good && fatal) {
    throw (n+'- '+m);
  }
  
}




For help and more information join our forum,follow the blog or follow me on twitter .


Comments