gridfs-locks
Advanced tools
Comparing version 0.0.6 to 1.0.0
@@ -0,2 +1,10 @@ | ||
### 1.0.0 | ||
- gridfs-locks now exclusively uses an event-emitter interface. See Readme for more information. | ||
### 0.0.6 | ||
- Documentation fixes | ||
- Removed console.log() | ||
### 0.0.5 | ||
@@ -3,0 +11,0 @@ |
505
index.js
@@ -7,7 +7,12 @@ /*********************************************************************** | ||
var eventEmitter = require('events').EventEmitter; | ||
var never = 8000000000000000; // never + now = 20000 years shy of max Date(), about 250,000 years from now | ||
// | ||
// Parameters: | ||
// | ||
// collection: a valid mongodb collection object | ||
// db: a valid mongodb connection object Mandatory | ||
// options object: | ||
// root: the string of the root mongodb collection name Default: 'fs' | ||
// w: mongo writeconcern Default: 1 | ||
@@ -19,70 +24,48 @@ // pollingInterval: Seconds between successive attempts to acquire a lock while waiting Default: 5 sec | ||
// | ||
// NOTE! -- Do not create a LockCollection directly using new, use the 'create' static method below | ||
// | ||
var LockCollection = exports.LockCollection = function(collection, options) { | ||
var LockCollection = exports.LockCollection = function(db, options) { | ||
var self = this; | ||
if (!(self instanceof LockCollection)) { return new LockCollection(db, options); } | ||
if(!(self instanceof LockCollection) || (!options || !options._created)) { | ||
throw new Error("LockCollections must be created using the 'LockCollection.create' static method") | ||
return; | ||
}; | ||
eventEmitter.call(self); // We are an eventEmitter | ||
if (typeof collection.find !== 'function') { | ||
throw new Error("Invalid collection parameter in LockCollection constructor") | ||
return; | ||
if (!db || typeof db.collection !== 'function') { | ||
return emitError(self, "LockCollection 'db' parameter must be a valid Mongodb connection object."); | ||
} | ||
self.writeConcern = options.w == null ? 1 : options.w; | ||
self.timeOut = options.timeOut || 0; // Locks do not poll by default | ||
self.pollingInterval = options.pollingInterval || 5; // 5 secs | ||
self.lockExpiration = options.lockExpiration || 0; // Never | ||
self.metaData = options.metaData || null; // None | ||
self.collection = collection; | ||
if (options && typeof options !== 'object') { | ||
return emitError(self, "LockCollection 'options' parameter must be an object."); | ||
} | ||
}; | ||
options = options || {}; | ||
// Static method for creation / initialization of a new LockCollection object. | ||
// | ||
// Use of a static method is necessary because the constructor can't be asyncronous | ||
// | ||
// Parameters: | ||
// | ||
// db: a valid mongodb connection object Mandatory | ||
// root: the string of the root mongodb collection name Default: 'fs' | ||
// options object: | ||
// w: mongo writeconcern Default: 1 | ||
// pollingInterval: Seconds between successive attempts to acquire a lock while waiting Default: 3 | ||
// lockExpiration: Seconds until an unrenewed lock expires in the database Default: 300 | ||
// timeOut: Seconds to poll when obtaining a lock that is not available. Default: 300 | ||
// callback: function(err, lockCollection) Mandatory. | ||
// | ||
LockCollection.create = function(db, root, options, callback) { | ||
if (!db || typeof db.collection !== 'function') { | ||
throw new Error("db is not a valid Mongodb connection object.") | ||
return; | ||
if (options.root && (typeof options.root !== 'string')) { | ||
return emitError(self, "LockCollection 'options.root' must be a string or falsy."); | ||
} | ||
if (root && (typeof root !== 'string')) { | ||
throw new Error("root must be a string or falsy.") | ||
return; | ||
} | ||
if (typeof callback !== 'function') { | ||
throw new Error("A callback function must be provided") | ||
return; | ||
} | ||
options = options || {}; | ||
options._created = true; // flag that this method was called | ||
root = root || 'fs'; | ||
collectionName = root + '.locks'; | ||
options.root = options.root || 'fs'; | ||
collectionName = options.root + '.locks'; | ||
db.collection(collectionName, function(err, collection) { | ||
if(err) return callback(err); | ||
if (err) { return emitError(self, err); } | ||
// Ensure unique files_id so there can only be one lock doc per file | ||
collection.ensureIndex([['files_id', 1]], {unique:true}, function(err, index) { | ||
if(err) return callback(err); | ||
callback(null, new LockCollection(collection, options)); | ||
if (err) { return emitError(self, err); } | ||
self.collection = collection; | ||
self.emit('ready'); | ||
}); | ||
}); | ||
self.writeConcern = options.w == null ? 1 : options.w; | ||
self.timeOut = options.timeOut || 0; // Locks do not poll by default | ||
self.pollingInterval = options.pollingInterval || 5; // 5 secs | ||
self.lockExpiration = options.lockExpiration || 0; // Never | ||
self.metaData = options.metaData || null; // None | ||
return self; | ||
}; | ||
LockCollection.prototype = eventEmitter.prototype; | ||
// Create a new Lock object | ||
@@ -101,9 +84,19 @@ // | ||
var Lock = exports.Lock = function(fileId, lockCollection, options) { | ||
if(!(this instanceof Lock)) return new Lock(fileId, lockCollection, options); | ||
if (!(this instanceof Lock)) return new Lock(fileId, lockCollection, options); | ||
var self = this; | ||
if (options && typeof options !== 'object') { | ||
return emitError(self, "Lock 'options' parameter must be an object."); | ||
} | ||
if (!(lockCollection instanceof LockCollection)) { | ||
throw new Error("Invalid lockCollection object."); | ||
return; | ||
return emitError(self, "Lock invalid 'lockCollection' object."); | ||
} | ||
var self = this; | ||
if (!lockCollection.collection) { | ||
return emitError(self, "Lock 'lockCollection' must be 'ready'."); | ||
} | ||
options = options || {}; | ||
@@ -115,5 +108,5 @@ self.lockCollection = lockCollection; | ||
self.pollingInterval = 1000*(options.pollingInterval || self.lockCollection.pollingInterval); | ||
self.lockExpiration = 1000*(options.lockExpiration || self.lockCollection.lockExpiration || 8000000000000); // Never | ||
self.lockExpireTime = new Date(self.timeCreated.getTime() + self.lockExpiration); // Fails in 20000 years?! | ||
self.timeOut = 1000*(options.timeOut || self.lockCollection.timeOut || 0); // Default to no timeout | ||
self.lockExpiration = 1000*(options.lockExpiration || self.lockCollection.lockExpiration); | ||
self.lockExpireTime = new Date(self.timeCreated.getTime() + (self.lockExpiration || never)); | ||
self.timeOut = 1000*(options.timeOut || self.lockCollection.timeOut); | ||
self.metaData = options.metaData || self.lockCollection.metaData; | ||
@@ -124,4 +117,57 @@ self.lockType = null; | ||
self.heldLock = null; | ||
self.expired = false; | ||
return self; | ||
}; | ||
Lock.prototype = eventEmitter.prototype; | ||
// Remove a currently held write lock. | ||
// | ||
// Parameters: | ||
// | ||
// Emits: | ||
// 'removed': | ||
// null: The lock document has been removed | ||
// 'error': | ||
// err: Any error that occurs | ||
// | ||
Lock.prototype.removeLock = function () { | ||
var self = this; | ||
var query = {files_id: self.fileId, write_lock: true}; | ||
if (!(self.heldLock) || self.expired) { | ||
return emitError(self, "Lock.removeLock cannot release an unheld lock."); | ||
} | ||
// self.timeCreated = new Date(); | ||
if (self.lockType === 'r') { | ||
return emitError(self, "Lock.removeLock cannot remove a readLock."); | ||
} else if (self.lockType[0] === 'w') { | ||
clearTimeout(self.expiresSoonTimeout); | ||
clearTimeout(self.expiredTimeout); | ||
self.collection.findAndRemove(query, [], {w: self.lockCollection.writeConcern}, function (err, doc) { | ||
if (err) { return emitError(self, err); } | ||
self.lockType = null; | ||
self.query = null; | ||
self.update = null; | ||
self.heldLock = null; | ||
if (doc == null) { | ||
return emitError(self, "Lock.removeLock Lock document not found in collection."); | ||
} | ||
doc.expires = self.timeCreated | ||
doc.write_lock = false | ||
self.emit('removed', doc); | ||
}); | ||
} else { | ||
return emitError(self, "Lock.removeLock invalid lockType."); | ||
} | ||
return self; // allow chaining | ||
}; | ||
// Release a currently held lock. | ||
@@ -131,6 +177,9 @@ // | ||
// | ||
// callback: function(err, doc) Mandatory. | ||
// doc: The new unheld lock document in the database | ||
// Emits: | ||
// 'released': | ||
// doc: The new unheld lock document in the database | ||
// 'error': | ||
// err: Any error that occurs | ||
// | ||
Lock.prototype.releaseLock = function(callback) { | ||
Lock.prototype.releaseLock = function () { | ||
@@ -141,27 +190,28 @@ var self = this; | ||
if (callback && typeof callback !== 'function') { | ||
throw new Error("Callback must be a function") | ||
if (!(self.heldLock) || self.expired) { | ||
return emitError(self, "Lock.releaseLock cannot release an unheld lock."); | ||
} | ||
function cb(err, doc) { | ||
if (callback) { | ||
callback(err, doc); | ||
} else if (err) { | ||
throw err; | ||
} | ||
} | ||
clearTimeout(self.expiresSoonTimeout); | ||
clearTimeout(self.expiredTimeout); | ||
if (!(self.heldLock)) { | ||
return cb(new Error("Cannot release an unheld lock.")); | ||
} | ||
if(self.lockType === 'r') { | ||
query = {files_id: self.fileId, read_locks: {$gt: 0}}; | ||
update = {$inc: {read_locks: -1}, $set: {meta: null}}; | ||
} else if(self.lockType[0] === 'w') { | ||
query = {files_id: self.fileId, write_lock: true}; | ||
update = {$set: {write_lock: false, meta: null}}; | ||
// self.timeCreated = new Date(); | ||
if (self.lockType === 'r') { | ||
releaseReadLock(self); | ||
} else if (self.lockType[0] === 'w') { | ||
releaseWriteLock(self); | ||
} else { | ||
return cb(new Error("Invalid lockType: " + self.lockType)); | ||
return emitError(self, "Lock.releaseLock invalid lockType."); | ||
} | ||
return self; // allow chaining | ||
}; | ||
var releaseWriteLock = function (self) { | ||
var query = {files_id: self.fileId, write_lock: true}, | ||
update = {$set: {write_lock: false, expires: self.timeCreated, meta: null}}; | ||
self.collection.findAndModify(query, [], update, {w: self.lockCollection.writeConcern, new: true}, function (err, doc) { | ||
if (err) { return emitError(self, err); } | ||
self.lockType = null; | ||
@@ -171,7 +221,54 @@ self.query = null; | ||
self.heldLock = null; | ||
if (err == null && doc == null) { | ||
err = new Error("Lock document not found in collection"); | ||
if (doc == null) { | ||
return emitError(self, "Lock.releaseLock Lock document not found in collection."); | ||
} | ||
cb(err, doc); | ||
self.emit('released', doc); | ||
}); | ||
} | ||
var releaseReadLock = function (self) { | ||
var query = {files_id: self.fileId, read_locks: {$gt: 1}}, | ||
update = {$inc: {read_locks: -1}, $set: {meta: null}}; | ||
// Case for read_locks > 1 | ||
self.collection.findAndModify(query, [], update, {w: self.lockCollection.writeConcern, new: true}, function (err, doc) { | ||
if (err) { return emitError(self, err); } | ||
if (doc) { | ||
self.lockType = null; | ||
self.query = null; | ||
self.update = null; | ||
self.heldLock = null; | ||
return self.emit('released', doc); | ||
} else { | ||
query = {files_id: self.fileId, read_locks: 1}; | ||
update = {$set: {read_locks: 0, expires: self.timeCreated, meta: null}}; | ||
// Case for read_locks == 1 | ||
self.collection.findAndModify(query, [], update, {w: self.lockCollection.writeConcern, new: true}, function (err, doc) { | ||
if (err) { return emitError(self, err); } | ||
if (doc) { | ||
self.lockType = null; | ||
self.query = null; | ||
self.update = null; | ||
self.heldLock = null; | ||
return self.emit('released', doc); | ||
} else { | ||
// If another readLock released between the above two findAndModify calls they can both fail... so keep trying. | ||
// console.log("Retrying to release read lock..."); | ||
// Avoid an infinite loop when lock document no longer exists | ||
self.collection.findOne({files_id: self.fileId, read_locks: {$gt: 0}}, function (err, doc) { | ||
if (err) { return emitError(self, err); } | ||
if (doc == null) { | ||
return emitError(self, "Lock.releaseLock Valid read Lock document not found in collection."); | ||
} else { | ||
self.releaseLock(); | ||
} | ||
}); | ||
} | ||
}); | ||
} | ||
}); | ||
}; | ||
@@ -184,18 +281,21 @@ | ||
// | ||
// Parameters: | ||
// Parameters: None | ||
// | ||
// callback: function(err, doc) Mandatory. | ||
// doc: The renewed lock document in the database | ||
// Emits: | ||
// 'renewed': | ||
// doc: The new lock document in the database | ||
// 'error': | ||
// err: Any error that occurs | ||
// | ||
Lock.prototype.renewLock = function(callback) { | ||
Lock.prototype.renewLock = function() { | ||
var self = this; | ||
if (typeof callback !== 'function') { | ||
throw new Error("A callback function must be provided") | ||
return; | ||
} | ||
if (!(self.heldLock)) { | ||
return callback(new Error("Cannot renew an unheld lock.")); | ||
return emitError(self, "Lock.renewLock cannot renew an unheld lock."); | ||
} | ||
self.lockExpireTime = new Date(new Date().getTime() + self.lockExpiration); | ||
self.query = null; | ||
clearTimeout(self.expiresSoonTimeout); | ||
clearTimeout(self.expiredTimeout); | ||
self.lockExpireTime = new Date(new Date().getTime() + (self.lockExpiration || never)); | ||
self.collection.findAndModify({files_id: self.fileId}, | ||
@@ -206,8 +306,10 @@ [], | ||
function (err, doc) { | ||
if (err) { return emitError(self, err); } | ||
if (doc == null) { return emitError(self, "Lock.renewLock document not found in collection"); } | ||
self.heldLock = doc; | ||
if (err == null && doc == null) { | ||
err = new Error("Lock document not found in collection"); | ||
} | ||
callback(err, doc); | ||
self.expiresSoonTimeout = setTimeout(emitExpiresSoonEvent.bind(self, ''), 0.9*(self.lockExpireTime - new Date() - self.pollingInterval)); | ||
self.expiredTimeout = setTimeout(emitExpiredEvent.bind(self, ''), (self.lockExpireTime - new Date() - self.pollingInterval)); | ||
return self.emit('renewed', doc); | ||
}); | ||
return self; | ||
}; | ||
@@ -222,24 +324,33 @@ | ||
// | ||
// Parameters: | ||
// Emits: | ||
// 'locked': | ||
// doc: The obtained lock document in the database | ||
// 'timed-out': | ||
// null | ||
// 'expires-soon': | ||
// null - This lock has exhausted 90% of its lifetime and will soon expire | ||
// 'expired': | ||
// null - This lock is no longer valid due to expiration | ||
// 'error': | ||
// err: Any error that occurs | ||
// | ||
// callback: function(err, doc) Mandatory. | ||
// doc: The obtained lock document in the database, or null if the timeout exceeded during polling | ||
// | ||
Lock.prototype.obtainReadLock = function(callback) { | ||
Lock.prototype.obtainReadLock = function() { | ||
var self = this; | ||
if (typeof callback !== 'function') { | ||
throw new Error("A callback function must be provided") | ||
return; | ||
} | ||
self.timeCreated = new Date(); | ||
if (self.heldLock) { | ||
return callback(new Error("Cannot obtain an already held lock.")); | ||
return emitError(self, "Lock.obtainReadLock cannot obtain an already held lock."); | ||
} | ||
// Ensure that lock document for files_id exists | ||
self.timeCreated = new Date(); | ||
initializeLockDoc(self, function (err, doc) { | ||
if(err) { return callback(err); } | ||
self.query = {files_id: self.fileId, $or: [{expires: {$lt: new Date(new Date() - 2000*self.lockCollection.pollingInterval)}}, {write_lock: false, write_req: false}]}; | ||
self.update = {$inc: {read_locks: 1, reads: 1}, $set: {write_lock: false, write_req: false, expires: self.lockExpireTime, meta: self.metaData}}; | ||
if (err) { return emitError(self, err); } | ||
self.query = {files_id: self.fileId, | ||
$or: [{expires: {$lt: new Date(new Date() - 2000*self.lockCollection.pollingInterval)}}, | ||
{write_lock: false, write_req: false}]}; | ||
self.update = {$inc: {read_locks: 1, reads: 1}, $set: {write_lock: false, write_req: false, meta: self.metaData}}; | ||
self.lockType = 'r'; | ||
return timeoutQuery(self, callback); | ||
self.expired = false; | ||
timeoutReadLockQuery(self); | ||
}); | ||
return self; | ||
}; | ||
@@ -257,4 +368,2 @@ | ||
// | ||
// callback: function(err, doc) Mandatory. | ||
// doc: The obtained lock document in the database, or null if the timeout exceeded during polling | ||
// testingOptions: Unit Testing options: | ||
@@ -265,40 +374,56 @@ // testCallback: optional, used by Unit testing to have a hook after the write request is written | ||
// | ||
Lock.prototype.obtainWriteLock = function(callback, testingOptions) { | ||
// Emits: | ||
// 'locked': | ||
// doc: The obtained lock document in the database | ||
// 'timed-out': | ||
// null - No parameters in callback | ||
// 'expires-soon': | ||
// null - This lock has exhausted 90% of its lifetime and will soon expire | ||
// 'expired': | ||
// null - This lock is no longer valid due to expiration | ||
// 'error': | ||
// err: Any error that occurs | ||
// | ||
Lock.prototype.obtainWriteLock = function(testingOptions) { | ||
var self = this; | ||
if (typeof callback !== 'function') { | ||
throw new Error("A callback function must be provided") | ||
return; | ||
} | ||
if (self.heldLock) { | ||
return callback(new Error("Cannot obtain an already held lock.")); | ||
return emitError(self, "Lock.obtainWriteLock cannot obtain an already held lock."); | ||
} | ||
testingOptions = testingOptions || {}; | ||
// Ensure that lock document for files_id exists | ||
self.timeCreated = new Date(); | ||
initializeLockDoc(self, function (err, doc) { | ||
if (err) { return callback(err); } | ||
self.query = {files_id: self.fileId, $or: [{expires: {$lt: new Date()}, write_req: true}, {write_lock: false, read_locks: 0}]}; | ||
self.update = {$set: {write_lock: true, write_req: false, read_locks: 0, expires: self.lockExpireTime, meta: self.metaData}, $inc:{writes: 1}}; | ||
if (err) { return emitError(self, err); } | ||
self.query = {files_id: self.fileId, | ||
$or: [{expires: {$lt: new Date()}, write_req: true}, | ||
{write_lock: false, read_locks: 0}]}; | ||
self.update = {$set: {write_lock: true, write_req: false, read_locks: 0, meta: self.metaData}, $inc:{writes: 1}}; | ||
self.expired = false; | ||
self.lockType = 'w'; | ||
return timeoutQuery(self, function (err, doc) { | ||
if (err || doc) return callback(err, doc); | ||
if (!testingOptions.testWriteReq) { | ||
// Clear the write_req flag, since this obtainWriteLock has timed out | ||
self.collection.findAndModify({files_id: self.fileId, write_req: true}, [], {$set: {write_req: false }}, {w: self.lockCollection.writeConcern, new: true}, function (err, doc) { | ||
callback(err, null); | ||
}); | ||
} else { | ||
callback(err, null); | ||
} | ||
}, testingOptions.testingCallback); | ||
timeoutWriteLockQuery(self, testingOptions); | ||
}); | ||
return self; | ||
}; | ||
// Private function to help with properly emitting errors | ||
var emitError = function (self, err) { | ||
if (typeof err == 'string') err = new Error(err); | ||
setImmediate(function () { self.emit('error', err); }); | ||
return self; | ||
} | ||
// Private function that ensures an initialized lock doc is in the database | ||
var initializeLockDoc = function (self, callback) { | ||
self.lockExpireTime = new Date(new Date().getTime() + self.lockExpiration); | ||
self.collection.findAndModify({files_id: self.fileId}, | ||
[], | ||
{$setOnInsert: {files_id: self.fileId, expires: self.lockExpireTime, read_locks: 0, write_lock: false, write_req: false, reads: 0, writes: 0, meta: null}}, | ||
{$setOnInsert: {files_id: self.fileId, | ||
expires: self.timeCreated, | ||
read_locks: 0, | ||
write_lock: false, | ||
write_req: false, | ||
reads: 0, | ||
writes: 0, | ||
meta: null}}, | ||
{w: self.lockCollection.writeConcern, upsert: true, new: true}, | ||
@@ -308,21 +433,80 @@ callback); | ||
// Private functions that implement expiration events | ||
var emitExpiredEvent = function () { | ||
var self = this; | ||
var heldLock = self.heldLock; | ||
// console.log("expiring", heldLock); | ||
self.heldLock = null; | ||
self.expired = true | ||
self.emit('expired', heldLock); | ||
} | ||
var emitExpiresSoonEvent = function () { | ||
var self = this; | ||
self.emit('expires-soon', self.heldLock); | ||
} | ||
// Private function that implements polling for locks in the database | ||
var timeoutQuery = function (self, callback, testingCallback) { | ||
self.update.$set.expires = self.lockExpireTime = new Date(new Date().getTime() + self.lockExpiration); | ||
var timeoutReadLockQuery = function (self, options) { | ||
options = options || {}; | ||
self.update.$set.expires = self.lockExpireTime = new Date(new Date().getTime() + (self.lockExpiration || never)); | ||
// Read locks can break writelocks with write_req after more than one polling cycle | ||
if (self.lockType === 'r') { | ||
self.query.$or[0].expires.$lt = new Date(new Date() - 2*self.pollingInterval) | ||
} else { | ||
self.query.$or[0].expires.$lt = new Date(); | ||
} | ||
self.collection.findAndModify(self.query, [], self.update, {w: self.lockCollection.writeConcern, new: true}, function (err, doc) { | ||
self.heldLock = doc; | ||
if(err || doc) return callback(err, doc); | ||
self.query.$or[0].expires.$lt = new Date(new Date() - 2*self.pollingInterval); | ||
self.collection.findAndModify(self.query, | ||
[], | ||
self.update, | ||
{w: self.lockCollection.writeConcern, new: true}, | ||
function (err, doc) { | ||
if (err) { return emitError(self, err); } | ||
if (!doc) { | ||
if(new Date() - self.timeCreated >= self.timeOut) { | ||
return self.emit('timed-out'); | ||
} | ||
return setTimeout(timeoutReadLockQuery, self.pollingInterval, self, options); | ||
} else { | ||
self.heldLock = doc; | ||
if (self.lockExpiration) { | ||
self.expiresSoonTimeout = setTimeout(emitExpiresSoonEvent.bind(self, ''), 0.9*(self.lockExpireTime - new Date() - self.pollingInterval)); | ||
self.expiredTimeout = setTimeout(emitExpiredEvent.bind(self, ''), (self.lockExpireTime - new Date() - self.pollingInterval)); | ||
} | ||
return self.emit('locked', doc); | ||
} | ||
} | ||
); | ||
}; | ||
// keep trying until timeout | ||
if(new Date() - self.timeCreated > self.timeOut) { | ||
return callback(null, null); | ||
} else { | ||
if (self.lockType === 'w') { | ||
// Private function that implements polling for locks in the database | ||
var timeoutWriteLockQuery = function (self, options) { | ||
options = options || {}; | ||
self.update.$set.expires = self.lockExpireTime = new Date(new Date().getTime() + (self.lockExpiration || never)); | ||
self.query.$or[0].expires.$lt = new Date(); | ||
self.collection.findAndModify(self.query, | ||
[], | ||
self.update, | ||
{w: self.lockCollection.writeConcern, new: true}, | ||
function (err, doc) { | ||
if (err) { return emitError(self, err); } | ||
if (doc) { | ||
self.heldLock = doc; | ||
if (self.lockExpiration) { | ||
self.expiresSoonTimeout = setTimeout(emitExpiresSoonEvent.bind(self, ''), 0.9*(self.lockExpireTime - new Date() - self.pollingInterval)); | ||
self.expiredTimeout = setTimeout(emitExpiredEvent.bind(self, ''), (self.lockExpireTime - new Date() - self.pollingInterval)); | ||
} | ||
return self.emit('locked', doc); | ||
} | ||
if (new Date() - self.timeCreated >= self.timeOut) { | ||
// Clear the write_req flag, since this obtainWriteLock has timed out | ||
self.collection.findAndModify({files_id: self.fileId, write_req: true}, | ||
[], | ||
{$set: {write_req: false}}, | ||
{w: self.lockCollection.writeConcern, new: true}, | ||
function (err, doc) { | ||
if (err) { return emitError(self, err); } | ||
} | ||
); | ||
return self.emit('timed-out'); | ||
} else { | ||
// write_req gets set every time because claimed write locks and timed out write requests clear it | ||
@@ -334,14 +518,9 @@ self.collection.findAndModify({files_id: self.fileId, write_req: false}, | ||
function (err, doc) { | ||
if(err) { return callback(err); } | ||
if (testingCallback && (typeof testingCallback == 'function')) { | ||
setImmediate(testingCallback); | ||
} | ||
return setTimeout(timeoutQuery, self.pollingInterval, self, callback); | ||
if (err) { return emitError(self, err); } | ||
self.emit('write-req-set'); | ||
}); | ||
} else { | ||
return setTimeout(timeoutQuery, self.pollingInterval, self, callback); | ||
return setTimeout(timeoutWriteLockQuery, self.pollingInterval, self, options); | ||
} | ||
} | ||
}); | ||
); | ||
}; | ||
{ | ||
"name": "gridfs-locks", | ||
"version": "0.0.6", | ||
"description": "Distributed read/write locking based on MongoDB, primarily designed to make GridFS safe for concurrent access", | ||
"version": "1.0.0", | ||
"description": "Distributed read/write locking based on MongoDB, designed to make GridFS safe for concurrent access", | ||
"main": "index.js", | ||
"dependencies": {}, | ||
"devDependencies": { | ||
"mongodb": "~1.3.23", | ||
"mongodb": ">=1.4.0", | ||
"coffee-script": "*", | ||
@@ -32,3 +32,3 @@ "mocha": "*" | ||
"engine": { | ||
"node": ">=0.10" | ||
"node": ">=0.10.25" | ||
}, | ||
@@ -35,0 +35,0 @@ "readmeFilename": "README.md", |
245
README.md
# gridfs-locks | ||
`gridfs-locks` implements distributed and [fair read/write locking](https://en.wikipedia.org/wiki/Readers-writer_lock) based on [MongoDB](http://www.mongodb.org/), and is specifically designed to make MongoDB's [GridFS](http://docs.mongodb.org/manual/reference/gridfs/) file-store safe for concurrent access. It is a [node.js](http://nodejs.org/) [npm package](https://www.npmjs.org/package/gridfs-locks) built on top of the [native `mongodb` driver](https://www.npmjs.org/package/mongodb), and is compatible with the native [GridStore](https://github.com/mongodb/node-mongodb-native/blob/master/docs/gridfs.md) implementation. | ||
`gridfs-locks` implements distributed and [fair read/write locking](https://en.wikipedia.org/wiki/Readers-writer_lock) based on [MongoDB](http://www.mongodb.org/), and is specifically designed to make MongoDB's [GridFS](http://docs.mongodb.org/manual/reference/gridfs/) file-store [safe for concurrent access](https://jira.mongodb.org/browse/NODE-157). It is a [node.js](http://nodejs.org/) [npm package](https://www.npmjs.org/package/gridfs-locks) built on top of the [native `mongodb` driver](https://www.npmjs.org/package/mongodb), and is compatible with the native [GridStore](https://github.com/mongodb/node-mongodb-native/blob/master/docs/gridfs.md) implementation. | ||
NOTE: if you are a [gridfs-stream](https://www.npmjs.org/package/gridfs-stream) user, but need the locking capabilities of this package, you should check out [gridfs-locking-stream](https://www.npmjs.org/package/gridfs-locking-stream). It is basically gridfs-stream + gridfs-locks | ||
NOTE: if you use [gridfs-stream](https://www.npmjs.org/package/gridfs-stream) and need the locking capabilities of this package (and you probably do... see the "Why?" section at the bottom of this README), you should check out [gridfs-locking-stream](https://www.npmjs.org/package/gridfs-locking-stream). It is basically gridfs-stream + gridfs-locks. | ||
## Why? | ||
## What's new in v1.0.0 | ||
Following the [semantic versioning](http://semver.org/) spec, version 1.0.0 contains a few breaking changes from the prototype 0.0.x of `gridfs-locks`. The main difference is that v1.0.0 Lock and LockCollection objects are now [event-emitters](http://nodejs.org/api/events.html). There are three primary impacts of these changes: | ||
I know what you're thinking: | ||
- why does there need to be yet another locking library for node? | ||
- why not do this using [Redis](http://redis.io/), or better yet use one of the [existing Redis solutions](https://github.com/search?q=redis+locks&search_target=global)? | ||
- wait, safe concurrent access [isn't baked into MongoDB GridFS](https://jira.mongodb.org/browse/NODE-157)? | ||
1. All async callbacks have been eliminated from the API method parameter lists and replaced with events | ||
2. A much richer set of async events (eg. lock expirations) can now be observed and handled in a more intuitive way | ||
3. Locks for removed resources can be also be removed so they don't clutter up the lock collection | ||
I'll answer these in reverse order... GridFS is MongoDB's file store technology; really it's just a bunch of "data model" conventions making it possible to store binary blobs of arbitrarily large non-JSON data in MongoDB collections. And it's totally useful. | ||
However, the GridFS data model says nothing about how to safely synchronize attempted concurrent read/write access to stored files. This is a problem because GridFS uses two separate collections to store file metadata and data chunks, respectively. And since [MongoDB has no native support for atomic multi-operation transactions](http://docs.mongodb.org/manual/tutorial/isolate-sequence-of-operations/), this turns out to be a critical omission for almost any real-world use of GridFS. | ||
The official node.js native mongo driver's [GridStore](https://github.com/mongodb/node-mongodb-native/blob/master/docs/gridfs.md) library is only "safe" (won't throw errors and/or corrupt GridFS data files) under two possible scenarios: | ||
1. Once created, files are strictly read-only. After the initial write, they can never be changed or deleted. | ||
2. An application **never** attempts to access a file when any kind of write or delete is also in progress. | ||
Neither of these constraints is acceptable for most real applications likely to be built with node.js using MongoDB. The solution is an efficient and robust locking mechanism to enforce condition #2 above by properly synchronizing read/write accesses. That is what this package provides. | ||
[Redis](http://redis.io/) is an amazing tool and this task could be certainly be done using Redis, but in this case we are already using MongoDB and it also has the capability to get the job done, so adding an unnecessary dependency on another server technology is undesirable. | ||
I tailored this library to use MongoDB and mirror the GridFS data model in the hopes that it may inspire the MongoDB team to add official concurrency features to a future version of the GridFS specification. In the meantime, this library will hopefully suffice in making GridFS generally useful for real world applications. I welcome all feedback. | ||
### Installation | ||
@@ -54,30 +39,34 @@ | ||
// Create a lock collection alongside the GridFS collections | ||
LockCollection.create(db, 'fs', {}, function (err, lockColl) { | ||
var lockColl = LockCollection(db, { root: 'fs', | ||
timeOut: 60, | ||
pollingInterval: 5, | ||
lockExpiration: 30 }); | ||
// Add error event handler for lockColl | ||
// 'ready' event when the collection is ready to use | ||
lockColl.on('ready', function () { | ||
var ID = something; // ID is the unique _id of a GridFS file, or whatever... | ||
// Create a lock object for ID | ||
var lock = new Lock(ID, lockColl, {}); | ||
var lock = Lock(ID, lockColl, {}); // Options can override collection settings | ||
// Request a write lock | ||
lock.obtainWriteLock(function(err, res) { | ||
if (err || res == null) { | ||
// Error or didn't get the lock... | ||
} | ||
lock.obtainWriteLock() | ||
// Event emitted when lock obtained | ||
lock.on('locked', function(ld) { | ||
// Write to a gridFS file, do generally unsafe things | ||
// Don't forget! | ||
lock.releaseLock(function (err, res) {}); | ||
lock.releaseLock(); | ||
}); | ||
// Another lock on same resource ID | ||
// Another lock on same resource ID, use of 'new' is optional | ||
var lock2 = new Lock(ID, lockColl, {}); | ||
// Request a read lock | ||
lock2.obtainReadLock(function(err, res) { | ||
if (err || res == null) { | ||
// Error or didn't get the lock... | ||
} | ||
// Request a read lock. Note calls can be chained... | ||
lock2.obtainReadLock().on('locked', function(ld) { | ||
@@ -87,6 +76,17 @@ // Read from a GridFS file, safe in the knowledge that some | ||
// Don't forget! | ||
lock2.releaseLock(function (err, res) {}); | ||
// Release the lock, and then reuse it to remove the resource | ||
lock2.releaseLock().on('released', function () { | ||
lock2.obtainWriteLock().on('locked', function () { | ||
// Remove the file/resource/whatever | ||
lock2.removeLock(); // Remove the lock from the collection | ||
} | ||
); | ||
} | ||
); | ||
}); | ||
// Add error and timed-out event handlers for lock and lock2 | ||
}); | ||
@@ -106,11 +106,13 @@ }); | ||
### LockCollection.create() | ||
### LockCollection(db, options) | ||
Create a new lock collection. **Note**: Do not use `new LockCollection()` because collection creation needs an async callback. | ||
Create a new lock collection. | ||
```js | ||
LockCollection.create( | ||
// using 'new' is optional | ||
var lockColl = new LockCollection( | ||
db, // Must be an open mongodb connection object | ||
'fs', // Root name for the collection. Will become "fs.locks" | ||
{ // Options: All can be overridden per lock. | ||
{ // Options: All except 'root' can be overridden per lock. | ||
root: 'fs', // root name for the collection. Will become "fs.locks" | ||
lockExpiration: 300, // seconds until a lock expires in the database Default: Never expire | ||
@@ -121,8 +123,19 @@ timeOut: 30, // seconds to poll when obtaining a lock that is not available. Default: Do not poll | ||
w: 1 // mongodb write-concern Default: 1 | ||
}, | ||
function (err, lockColl) { // Required callback | ||
// err: any database errors or problems with parameters | ||
// lockColl: a LockCollection object if successful | ||
} | ||
); | ||
}); | ||
// Emits events: | ||
// event: 'ready' - emitted when the collection is ready to use | ||
lockColl.on('ready', function () { | ||
// Use collection to create/use locks, etc. | ||
}); | ||
// event: 'error' - emitted in the case of a database or other unrecoverable error. 'ready' will not be emitted | ||
// No listener for 'error' events will result in throws in case of errors (node.js default behavior) | ||
lockColl.on('error', function (err) { | ||
// Handle error | ||
}); | ||
``` | ||
@@ -135,2 +148,5 @@ | ||
```js | ||
// using 'new' is optional | ||
lock = new Lock( | ||
@@ -147,2 +163,62 @@ Id, // Unique identifier for resource being locked. Type must be compatible with mongodb `_id` | ||
// Emits events: | ||
// event: 'error' - emitted in the case of a database or other unrecoverable error. | ||
// No listener for 'error' events will result in throws in case of errors (node.js default behavior) | ||
lock.on('error', function (err) { | ||
// Handle error | ||
}); | ||
// event: 'locked' - A lock has been obtained. Supplies the current lock document | ||
// see obtainReadLock() and obtainWriteLock() methods below | ||
lock.on('locked', function (ld) { // provides current lock document | ||
// Use locked resource... | ||
}); | ||
// event: 'timed-out' - A timeout has occurred while waiting to obtain an unavailable lock | ||
// This event only occurs when timeOut != 0 | ||
// see obtainReadLock() and obtainWriteLock() methods below | ||
lock.on('timed-out', function () { | ||
// Handle timeout... | ||
}); | ||
// event: 'released' - A held lock was successfully released | ||
// see releaseLock() method below | ||
lock.on('released', function (ld) { | ||
// do something else | ||
}); | ||
// event: 'removed' - A held write lock was successfully removed from the lock collection | ||
// see removeLock() method below | ||
lock.on('removed', function (ld) { | ||
// do something else | ||
}); | ||
// The following three events only occur when lockExpiration != 0 | ||
// event: 'expires-soon' - warning ~90% of the lifetime of this lock has passed. | ||
// Either release or renew the lock, see releaseLock() and renewLock() methods below | ||
lock.on('expires-soon', function (ld) { // provides current lock document | ||
// release or renew... | ||
}); | ||
// event: 'renewed' - A held lock was successfully renewed for another lifetime | ||
// see renewLock() method below | ||
lock.on('renewed', function (ld) { | ||
// continue using lock | ||
}); | ||
// event: 'expired' - the lifetime of this lock has passed. | ||
// It is no longer safe to use the underlying resource without obtaining a new lock | ||
lock.on('expired', function (ld) { | ||
// handle expiration | ||
}); | ||
``` | ||
@@ -155,7 +231,10 @@ | ||
```js | ||
lock.obtainReadLock( | ||
function (err, l) { // Required callback | ||
// err: any database error | ||
// l: the lock document obtained. If null, the attempt failed or timed out | ||
lock.obtainReadLock().on('locked', | ||
function (ld) { | ||
// Use lock | ||
} | ||
).on('timed-out', | ||
function () { | ||
// Didn't get lock | ||
} | ||
); | ||
@@ -169,7 +248,10 @@ ``` | ||
```js | ||
lock.obtainWriteLock( | ||
function (err, l) { // Required callback | ||
// err: any database error | ||
// l: the lock document obtained. If null, the attempt failed or timed out | ||
lock.obtainWriteLock().on('locked', | ||
function (ld) { | ||
// Use lock | ||
} | ||
).on('timed-out', | ||
function () { | ||
// Didn't get lock | ||
} | ||
); | ||
@@ -183,6 +265,5 @@ ``` | ||
```js | ||
lock.releaseLock( | ||
function (err, l) { // This callback is optional, will throw on error if omitted | ||
// err: any database errors or lock document not found | ||
// l: the freed lock document | ||
lock.releaseLock().on('released', | ||
function (ld) { | ||
// No need to listen for this for no reason | ||
} | ||
@@ -192,2 +273,13 @@ ); | ||
### lock.removeLock() | ||
Remove a held write lock from the lock collection. Appropriate to use when the write lock is obtained to delete a resource. | ||
```js | ||
lock.removeLock().on('removed', | ||
function (ld) { | ||
// No need to listen for this for no reason | ||
} | ||
); | ||
``` | ||
### lock.renewLock() | ||
@@ -198,6 +290,9 @@ | ||
```js | ||
lock.renewLock( | ||
function (err, l) { // Required callback | ||
// err: any database error or lock document not found | ||
// l: the lock document obtained. | ||
lock.on('expires-soon', | ||
function() { | ||
lock.renewLock().on('renewed', | ||
function (ld) { | ||
// Keep using lock | ||
} | ||
); | ||
} | ||
@@ -221,3 +316,23 @@ ); | ||
# Why? | ||
I know what you're thinking: | ||
- why does there need to be yet another locking library for node? | ||
- why not do this using [Redis](http://redis.io/), or better yet use one of the [existing Redis solutions](https://github.com/search?q=redis+locks&search_target=global)? | ||
- wait, safe concurrent access [isn't already baked into MongoDB GridFS](https://jira.mongodb.org/browse/NODE-157)? | ||
I'll answer these in reverse order... GridFS is MongoDB's file store technology; really it's just a bunch of "data model" conventions making it possible to store binary blobs of arbitrarily large non-JSON data in MongoDB collections. And it's totally useful. | ||
However, the GridFS data model says nothing about how to safely synchronize attempted concurrent read/write access to stored files. This is a problem because GridFS uses two separate collections to store file metadata and data chunks, respectively. And since [MongoDB has no native support for atomic multi-operation transactions](http://docs.mongodb.org/manual/tutorial/isolate-sequence-of-operations/), this turns out to be a critical omission for almost any real-world use of GridFS. | ||
The official node.js native mongo driver's [GridStore](https://github.com/mongodb/node-mongodb-native/blob/master/docs/gridfs.md) library is only "safe" (won't throw errors and/or corrupt GridFS data files) under two possible scenarios: | ||
1. Once created, files are strictly read-only. After the initial write, they can never be changed or deleted. | ||
2. An application **never** attempts to access a file when any kind of write or delete is also in progress. | ||
Neither of these constraints is acceptable for most real applications likely to be built with node.js using MongoDB. The solution is an efficient and robust locking mechanism to enforce condition #2 above by properly synchronizing read/write accesses. That is what this package provides. | ||
[Redis](http://redis.io/) is an amazing tool and this task could be certainly be done using Redis, but in this case we are already using MongoDB and it also has the capability to get the job done, so adding an unnecessary dependency on another server technology is undesirable. | ||
I tailored this library to use MongoDB and mirror the GridFS data model in the hopes that it may inspire the MongoDB team to add official concurrency features to a future version of the GridFS specification. In the meantime, this library will hopefully suffice in making GridFS generally useful for real world applications. I welcome all feedback. | ||
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
78790
11
593
1
327