The LockService gives the capability to do this
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
With the important addition
ApproachNote 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 or by including library Mpv7vUR0126U53sfSMXsAPai_d-phDA33 or on github Examplecreate a named lock var namedLock = new NamedLock(); set a key namedLock .setKey('some name'); 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 thingsadjust 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 fancyRun 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'; }); 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 instancesThere 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()); }
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()); } }
SetSameInstanceLockedThe 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
var globalLock = new NamedLock().setKey(resource).setSameInstanceLocked(true); globalLock.lock(); //// has to wait A test with multiple triggersIn 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
|
Services > Desktop Liberation - the definitive resource for Google Apps Script and Microsoft Office automation > Google Apps Scripts snippets >