-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.js
103 lines (83 loc) · 2.84 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
'use strict';
var crypto = require('crypto');
var _ = require('lodash');
var redis = require('redis');
var uuid = require('node-uuid');
var Scripto = require('redis-scripto');
var luaUnlock = require('fs').readFileSync(__dirname + '/unlock.lua', {encoding: 'utf8'});
function Lock (name, options) {
if (!(this instanceof Lock)) {
return new Lock(name, options);
}
if (!name || ! _.isString(name)) {
throw new Error('Lock name not specified');
}
this.name = name;
// make sure keys are constant length
this.key = 'lock:' + crypto.createHash('sha1').update(this.name).digest('hex').substr(0, 10);
this.options = _.assign({
ttl: 5000, // redis-lock's default
retryDelay: 50, // redis-lock's default
maxAttempts: null // keep trying forever
}, options);
this.client = this.options.client || redis.createClient();
this.scriptManager = this.options.scriptManager || new Scripto(this.client);
this.scriptManager.load({
unlock: luaUnlock
});
}
Lock.prototype.lock = function lock (options, callback) {
if (!callback) {
callback = arguments[0];
options = null;
}
options = _.assign({}, this.options, options);
// default to constant delay
if (!_.isFunction(options.retryDelay)) {
var retryDelay = options.retryDelay;
options.retryDelay = function () {
return retryDelay;
};
}
var lock = this;
var attempts = 0;
var doLock = function () {
attempts++;
// we always try to acquire the lock first and then check maxAttempts, since there's no point setting maxAttempts
// equal to 0
var value = options.value || uuid.v1();
lock.client.set(lock.key, value, 'NX', 'PX', options.ttl, function (err, locked) {
if (err) {
return callback(err);
}
if (!locked) {
if (options.maxAttempts && options.maxAttempts <= attempts) {
return callback(null, false);
}
// try again later
setTimeout(doLock, options.retryDelay.call(lock));
} else {
// we got the lock
lock.unlock = function unlock (callback) { // override Lock.prototype.unlock
lock.scriptManager.run('unlock', [lock.key], [value], callback);
};
callback(null, lock.unlock);
}
});
};
doLock();
};
Lock.prototype.tryLock = function tryLock (options, callback) {
options = _.assign({maxAttempts: 1}, options);
return this.lock(options, callback);
};
/**
* The same unlock function that's passed to `lock`. Useful if you want to unlock outside the scope of the `lock`
* function. Since code passed to the `lock` function might still be using the lock, this method should be used with
* caution.
*/
Lock.prototype.unlock = function (callback) {
// This method is overridden when we lock. If this function got called, it means we don't hold the lock.
process.nextTick(callback);
};
module.exports = Lock;