Skip to content

Commit

Permalink
Add promises support.
Browse files Browse the repository at this point in the history
  • Loading branch information
gregberge committed Jan 29, 2015
1 parent da9cc83 commit 776e0e1
Show file tree
Hide file tree
Showing 7 changed files with 243 additions and 349 deletions.
38 changes: 16 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
[![Dependency Status](https://david-dm.org/neoziro/locky.svg?theme=shields.io)](https://david-dm.org/neoziro/locky)
[![devDependency Status](https://david-dm.org/neoziro/locky/dev-status.svg?theme=shields.io)](https://david-dm.org/neoziro/locky#info=devDependencies)

Resource locking system.
Fast resource locking system based on redis.

## Install

Expand All @@ -20,16 +20,16 @@ var Locky = require('locky');
var locky = new Locky();

// Lock the resource 'article:12' with the locker 20.
locky.lock('article:12', 20, cb);
locky.lock('article:12', 20).then(...);

// Refresh the lock TTL of the resource 'article:12'.
locky.refresh('article:12', cb);
locky.refresh('article:12').then(...);

// Unlock the resource 'article:12.
locky.unlock('article:12', cb);
locky.unlock('article:12').then(...);

// Get the locker of the resource 'article:12'.
locky.getLocker('article:12', cb);
locky.getLocker('article:12').then(...);
```

### new Locky(options)
Expand Down Expand Up @@ -80,7 +80,7 @@ new Locky({
})
```

### locky.lock(opts, callback)
### locky.lock(options, [callback])

Lock a resource for a locker.

Expand All @@ -92,7 +92,9 @@ locky.lock({
resource: 'article:23',
locker: 20,
force: false
}, function (err, res) { ... });
}).then(function (locked) {
console.log(locked); // true the lock has been taken
});
```

#### resource
Expand All @@ -113,39 +115,31 @@ Type: `Boolean`

Should we take a lock if it's already locked?

#### callback(err, res)

##### res

Type: `Boolean`

Was the lock successful? If so you will also get a `lock` event.

### locky.refresh(resource, callback)
### locky.refresh(resource, [callback])

Refresh the lock ttl of a resource, if the resource is not locked, do nothing.

```js
// Refresh the resource "article:23".
locky.refresh('article:23', function (err) { ... });
locky.refresh('article:23').then(...);
```

### locky.unlock(resource, callback)
### locky.unlock(resource, [callback])

Unlock a resource, if the resource is not locked, do nothing.

```js
// Unlock the resource "article:23".
locky.unlock('article:23', function (err) { ... });
locky.unlock('article:23').then(...);
```

### locky.getLocker(resource, callback)
### locky.getLocker(resource, [callback])

Return the locker of a resource, if the resource is not locked, return `null`.

```js
// Return the locker of the resource "article:23".
locky.getLocker('article:23', function (err, locker) { ... });
locky.getLocker('article:23').then(...);
```

### Events
Expand Down Expand Up @@ -176,4 +170,4 @@ locky.on('expire', function (resource) { ... });

## License

MIT
MIT
6 changes: 1 addition & 5 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1 @@
/**
* Expose module.
*/

module.exports = require('./lib/locky');
module.exports = require('./lib/locky');
192 changes: 83 additions & 109 deletions lib/locky.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ var events = require('events');
var util = require('util');
var _ = require('lodash');
var resourceKey = require('./resource-key');
var redis = require('redis');
var redis = require('then-redis');
var Promise = require('bluebird');

/**
* Expose module.
Expand All @@ -23,20 +24,21 @@ module.exports = Locky;
*/

function Locky(options) {
var locky = this;

options = _.defaults(options || {}, {
ttl: null,
redis: {}
});

function asyncIdentity(obj, cb) {
cb(null, obj);
}
locky.ttl = options.ttl;
locky.redis = locky._createRedisClient(options.redis);

this.ttl = options.ttl;
this.ttlSecond = Math.ceil(options.ttl / 1000); // Convert TTL to second (support redis < 2.6)
this.redis = this._createRedisClient(options.redis);
this.redis.on('error', function (err) {
console.log(err);
});

events.EventEmitter.call(this);
events.EventEmitter.call(locky);
}

/**
Expand All @@ -48,143 +50,120 @@ util.inherits(Locky, events.EventEmitter);
/**
* Try to lock a resource using a locker identifier.
*
* @param {string|number} opts.resource resource identifier to lock
* @param {string|number} opts.locker locker identifier
* @param {boolean} opts.force force gaining lock even if it's taken
* @param {lock~callback} cb
*/

/**
* Callback called with lock result
* @callback lock~cb
* @param {?error} err
* @param {boolean} res did we managed to get the lock or not
* @param {object} options
* @param {string|number} options.resource Resource identifier to lock
* @param {string|number} options.locker Locker identifier
* @param {boolean} options.force Force gaining lock even if it's taken
* @param {Promise} [callback] Optional callback
*/

Locky.prototype.lock = function lock(opts, callback) {
var resource = opts.resource;
var locker = opts.locker;
var force = opts.force;
Locky.prototype.lock = function lock(options, callback) {
var locky = this;

// was the lock successful?
var success;
options = options || {};

// Format key with resource id.
var key = resourceKey.format(resource);
var key = resourceKey.format(options.resource);

// Define the method to use.
var method = force ? 'set' : 'setnx';

// Set the lock key.
this.redis[method](key, locker, setDone.bind(this));

function setDone(err, res) {
success = res === 1 || res === 'OK';

if (err !== null) return hadError.call(this, err);

if (!this.ttlSecond || !success) {
return lockDone.call(this);
}

this.redis.expire(key, this.ttlSecond, expireSet.bind(this));
}

function expireSet(err) {
if (err) return hadError.call(this, err);
this._listenExpiration(resource);
lockDone.call(this);
}

function lockDone() {
if (success) this.emit('lock', resource, locker);
if (callback) callback(null, success);
}

function hadError(err) {
if (!callback) return this.emit('error', err);

callback(err);
}
var method = options.force ? 'set' : 'setnx';

// Set the lock key.
return locky.redis[method](key, options.locker)
.then(function (res) {
var success = res === 1 || res === 'OK';

if (!locky.ttl || !success) return success;

return locky.redis.pexpire(key, locky.ttl)
.then(function () {
locky._listenExpiration(options.resource);
return success;
});
})
.then(function (success) {
if (success)
locky.emit('lock', options.resource, options.locker);
return success;
})
.nodeify(callback);
};

/**
* Refresh the lock ttl of a resource.
*
* @param {string} resource
* @param {function} callback
* @param {string} resource Resource
* @param {function} [callback] Optional callback
*/

Locky.prototype.refresh = function refresh(resource, callback) {
// If there is no TTL, do nothing.
if (! this.ttl) {
if (callback) callback();
return ;
}

// Format key with resource id.
var key = resourceKey.format(resource);

// Set the TTL of the key.
this.redis.expire(key, this.ttlSecond, callback);
this._listenExpiration(resource);
var locky = this;

return new Promise(function (resolve, reject) {
// If there is no TTL, do nothing.
if (!locky.ttl) return resolve();

// Format key with resource id.
var key = resourceKey.format(resource);

// Set the TTL of the key.
locky.redis.pexpire(key, locky.ttl).then(resolve, reject);
})
.then(function () {
locky._listenExpiration(resource);
})
.nodeify(callback);
};

/**
* Unlock a resource.
*
* @param {string} resource
* @param {function} callback
* @param {string} resource Resource
* @param {function} [callback] Optional callback
*/

Locky.prototype.unlock = function unlock(resource, callback) {
var locky = this;

// Format key with resource id.
var key = resourceKey.format(resource);

// Remove the key.
this.redis.del(key, function keyDeleted(err, res) {
if (err && callback) return callback(err);
if (err && ! callback) return this.redis.emit('error', err);


if (res !== 0) this.emit('unlock', resource);

if(callback) callback();
}.bind(this));
return locky.redis.del(key).then(function (res) {
if (res !== 0) locky.emit('unlock', resource);
})
.nodeify(callback);
};

/**
* Return the resource locker.
*
* @param {string} resource
* @param {function} callback
* @param {string} resource Resource
* @param {function} [callback] Optional callback
*/

Locky.prototype.getLocker = function getLocker(resource, callback) {
this.redis.get(resourceKey.format(resource), callback);
return this.redis.get(resourceKey.format(resource)).nodeify(callback);
};

/**
* Close the client.
*
* @param {function} callback
* @param {function} [callback] Optional callback
*/

Locky.prototype.close = function close(callback) {
this.redis.quit(callback);
return this.redis.quit().nodeify(callback);
};

/**
* Listen expiration.
*
* @param {string} resource
* @param {string} resource Resource
*/

Locky.prototype._listenExpiration = function _listenExpiration(resource) {
// We add a timeout to simulate the notification of expiring in redis.
// There is a lag of 1s to ensure that the redis key is expired (redis 2.4 sux).
var expireTime = this.ttlSecond * 1000 + 1000;
setTimeout(this._onExpire.bind(this, resource), expireTime);
setTimeout(this._onExpire.bind(this, resource), this.ttl);
};

/**
Expand All @@ -194,30 +173,25 @@ Locky.prototype._listenExpiration = function _listenExpiration(resource) {
*/

Locky.prototype._onExpire = function _onExpire(resource) {
this.getLocker(resource, function gotLocker(err, locker) {
if (err) return this.redis.emit('error', err);
var locky = this;

return locky.getLocker(resource).then(function (locker) {
// If there is a locker, the key has not expired.
if (locker) return ;
if (locker) return;

// Emit an expire event.
this.emit('expire', resource);

}.bind(this));
locky.emit('expire', resource);
});
};

/**
* Create the redis client.
*
* @param {object} options
* @param {object|function} options
*/

Locky.prototype._createRedisClient = function _createRedisClient(options) {
Locky.prototype._createRedisClient = function (options) {
if (_.isFunction(options)) return options();
return redis
.createClient(
options.port,
options.host,
_.omit(options, 'port', 'host')
);
};

return redis.createClient(options);
};
Loading

0 comments on commit 776e0e1

Please sign in to comment.