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 } ]