cassandra-server
Version:
A thin Javascript wrapper around Apache Cassandra to provide out of the box server support
253 lines (211 loc) • 6.84 kB
JavaScript
;
var childProcess = require('child_process'),
Client = require('cassandra-driver').Client,
EventEmitter = require('events').EventEmitter,
fs = require('fs-extra'),
path = require('path'),
Q = require('q'),
yaml = require('js-yaml');
// the server process
var child;
// the Cassandra client
var client;
// the Cassandra log levels
var levels = ['debug', 'info', 'warn'];
// the amount of time to wait before restarting the server
var restartWaitTime = 3000;
// the amount of time to wait before connecting the client
var startWaitTime = 5000;
/**
* Creates a YAML configuration file after overriding defaults with user options.
*
* <p>
* Cassandra expects it's configuration to reside in $CASSANDRA_DIR/conf/cassandra.yaml.
* After loading the default options from cassandra.json and overriding them with user provided options,
* this function converts the final object to YAML and writes it to the target location.
* </p>
*
* @param {Object} options - the user options
* @private
* @returns {Promise} - a promise that resolves to the merged server options
*/
function resolveOptions(options) {
var jsonFileName = path.join(__dirname, 'cassandra.json');
cassandra.emit('log', 'debug', 'Reading default configuration: ' + jsonFileName);
return Q.nfcall(fs.readFile, jsonFileName, 'utf-8')
.then(function(json) {
var defaults = JSON.parse(json);
// overriding default options
for(var property in options) {
if(options.hasOwnProperty(property)) {
defaults[property] = options[property];
}
}
cassandra.emit('log', 'debug', 'Resolved Cassandra options are: ' + JSON.stringify(defaults));
var yamlFileName = path.join(__dirname, 'apache-cassandra-2.1.0', 'conf', 'cassandra.yaml');
cassandra.emit('log', 'debug', 'Creating YAML configuration file: ' + yamlFileName);
return Q.nfcall(fs.writeFile, yamlFileName, yaml.dump(defaults))
.then(function() {
return defaults;
});
});
}
var cassandra = Object.create(EventEmitter.prototype);
module.exports = cassandra;
/**
* Nukes the entire database.
* @returns {Promise}
*/
cassandra.nuke = function() {
var deferred = Q.defer();
// if running
if(child) {
var error = new Error('Cannot delete database when Cassandra is running');
cassandra.emit('log', 'error', error);
deferred.reject(error);
return deferred.promise;
}
cassandra.emit('log', 'warn', 'Deleting all data in the database');
return Q.nfcall(fs.remove, path.join(__dirname, 'apache-cassandra-2.1.0', 'data'))
.then(Q.nfcall(fs.remove, path.join(__dirname, 'apache-cassandra-2.1.0', 'logs')));
};
/**
* Restarts the server.
* @param {Object} [options] - the server options
* @returns {Promise} - a promise that resolves to the Cassandra client
*/
cassandra.restart = function(options) {
var deferred = Q.defer();
cassandra.stop()
.then(function() {
cassandra.emit('log', 'info', 'Waiting ' + restartWaitTime + ' seconds before starting');
setTimeout(function() {
cassandra.start(options)
.then(function(client) {
deferred.resolve(client);
})
.catch(function(err) {
deferred.reject(err);
});
}, restartWaitTime);
});
return deferred.promise;
};
/**
* Starts the server.
* @param {Object} [options] - the server options
* @returns {Promise} - a promise that resolves to the Cassandra client
*/
cassandra.start = function(options) {
options = options || {};
var deferred = Q.defer(),
timeoutId;
// if already started
if(child) {
cassandra.emit('log', 'info', 'Cassandra is already started');
return deferred.resolve();
}
cassandra.emit('log', 'info', 'Starting Cassandra with options: ' + JSON.stringify(options));
resolveOptions(options)
// start the server
.then(function(merged) {
child = childProcess.spawn(path.join(__dirname, 'apache-cassandra-2.1.0', 'bin', 'cassandra'), ['-f']);
// error handler
child.on('error', function(err) {
var error = new Error('Cassandra process error');
error.cause = err;
cassandra.emit('log', 'error', error);
// if we're starting up then reject the promise
if(timeoutId) {
clearTimeout(timeoutId);
timeoutId = undefined;
deferred.reject(error);
}
});
// exit handler
child.on('exit', function(code) {
cassandra.emit('log', 'info', 'Cassandra exited with code: ' + code);
// if we're starting up then reject the promise
if(timeoutId) {
clearTimeout(timeoutId);
timeoutId = undefined;
deferred.reject(new Error('Could not start Cassandra'));
}
});
// stderr handler
child.stderr.on('data', function(data) {
cassandra.emit('log', 'stderr', data.toString());
});
// track the last log level and use it for lines that don't have a log level prefix
var lastLevel;
// stdout listener
child.stdout.on('data', function(data) {
var line = data.toString(),
isLogLine = false;
// for each log level
for(var index = 0; index < levels.length; index++) {
var regexp = new RegExp('^' + levels[index], 'i');
// if the line is prefixed by a log level
if(line.match(regexp)) {
lastLevel = levels[index];
cassandra.emit('log', levels[index], line.replace(regexp, '').trim());
isLogLine = true;
break;
}
}
// if not a log line then use the last level logged
if(!isLogLine) {
cassandra.emit('log', lastLevel, line.trim());
}
});
// give it time to start
timeoutId = setTimeout(function() {
var seeds = merged.seed_provider[0].parameters[0].seeds;
client = new Client({contactPoints: seeds});
// resolve the startup promise when the client connects
Q.ninvoke(client, 'connect')
.then(function() {
timeoutId = undefined;
cassandra.emit('log', 'info', 'Server started');
deferred.resolve();
})
.catch(function(err) {
timeoutId = undefined;
var error = new Error('Could not connect to Cassandra');
error.cause = err;
cassandra.emit('log', 'error', error);
// since the client could not connect we may as well kill the server
cassandra.stop()
.done(function() {
deferred.reject(error);
});
})
}, startWaitTime);
});
return deferred.promise;
};
/**
* Stops the server.
* @returns {Promise}
*/
cassandra.stop = function() {
// if already stopped
if(!child) {
cassandra.emit('log', 'info', 'Cassandra is already stopped');
return Q();
}
// function to stop the server
function doStop() {
cassandra.emit('log', 'info', 'Stopping Cassandra');
child.kill();
child = client = undefined;
return Q();
}
// if the client couldn't connect
if(!client) {
return doStop();
}
// shutdown the client
return Q.ninvoke(client, 'shutdown')
.then(doStop);
};