Socket
Socket
Sign inDemoInstall

gridfs-locks

Package Overview
Dependencies
Maintainers
1
Versions
23
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

gridfs-locks - npm Package Compare versions

Comparing version 0.0.6 to 1.0.0

.npmignore

8

HISTORY.md

@@ -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",

# 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

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc