Locking in Node.js
Ran into a odd situation with our Node.js project. Normally, when
you request access to a database (in our case, we're using MongoDB),
you give it a callback, which is executed when the connection pool has
been initialized and is ready for access.
mongodb.connect( dburl, options, function( err, conn ) {
if ( err ) {
// Uh, barf?
}
else {
// Do some work, like:
// conn.find(..);
}
});
Of course, calling connect()
for each HTTP request is actually
pretty bad since connect()
is calling out to a pool. A better approach is to wrap up your HTTP server in the callback.
mongodb.connect( dburl, options, function( err, conn ) {
if ( err ) {
console.warn("Can't connect to MongoDB", err);
}
else {
app.listen(config.port, config.host);
}
});
This works fine on our HTTP server, however, this project is
cloud-based, meaning we have many micro services running, and a
couple of them didn't have a single start function like the HTTP
gateway server.
One of my engineers asked if we couldn't make create a getConnection()
function in our database interface module that any function could call.
Good idea, but the problem with the Mongo-Node driver is that the first
request starts up the pool, and it currently can't handle multiple requests
until the pool is setup.
Ah, a challenge.
What we need is a function that:
- Allow the first request access to the database
- Block all other database access requests
- Once the first request is done, release the other requests
I created a lock.js
module that my database interface module would require.
The module has a lock.get()
that return true if the lock is free,
false otherwise. This function takes a callback, which is executed
once the lock is free. My database module's getConnection()
function
looked like:
var getConnection = function( callback ) {
if( lock.get( callback ) ) {
mongodb.connect( dburl, dboptions, function( err, conn ) {
if( err ) {
callback( err );
}
else {
callback( null, conn );
lock.free( this, [ null, conn ] );
}
});
} // end if lock.get
The lock.free()
function allows all of the previously blocked
functions to run, by calling .apply()
on it. That is why we
pass in a this
variable and an array of parameters (a null
of the error, and the conn
database connection variable).
The code for lock.js
starts with a couple of module-level
variables: a variable to lock, and an array of callbacks of all
the blocked requesters:
/**
* Default locked variable, set it to `true` until free
*/
var locked = false;
var requesters = [];
The getLock()
is pretty simple, since in JavaScript each function is
autonomous (until a function is called):
/**
* Return current lock status. If it's locked, put the callback into
* the `requesters` array to be called later.
*
* @param {function} callback run once lock is free.
*/
var getLock = function( cb ) {
if( locked ) {
requesters.push( cb );
return false;
}
else {
locked = true;
return true;
}
}; // end var getLock
exports.get = getLock;
This function sets the locked
variable to true, if it is not locked,
and if it is, pushes the callback on the array.
The freeLock()
function does an .apply()
on each callback, as in:
/**
* Free lock status. Also run the all the methods in `requesters`
* array using the given environment specified by the `that`
*
* @param {Object} that running environment
* @param {Array} params arguments passed to `requester`.
*/
var freeLock = function( that, params ) {
locked = false;
for ( var r = 0; r < requesters.length; r++ ) {
requesters[r].apply( that, params );
} // end for
}; // end var freeLock
exports.free = freeLock;
Tell others about this article: