blob: 4f1449243eb635d0fa107b20cd41dc7a0f050083 [file]
/*
* Copyright DataStax, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict';
const helper = require('../test-helper');
const http = require('http');
const spawn = require('child_process').spawn;
const util = require('util');
const fs = require('fs');
const utils = require('../../lib/utils.js');
const Client = require('../../lib/client.js');
const simulacronHelper = {
_execute: function(processName, params, cb) {
const originalProcessName = processName;
// If process hasn't completed in 10 seconds.
let timeout = undefined;
if(cb) {
timeout = setTimeout(() => cb('Timed out while waiting for ' + processName + ' to complete.'), 10000);
}
const p = spawn(processName, params, {});
p.stdout.setEncoding('utf8');
p.stderr.setEncoding('utf8');
p.stdout.on('data', function (data) {
helper.trace('%s_out> %s', originalProcessName, data);
});
p.stderr.on('data', function (data) {
helper.trace('%s_err> %s', originalProcessName, data);
});
p.on('close', function (code) {
helper.trace('%s exited with code %d', originalProcessName, code);
if(cb) {
clearTimeout(timeout);
if (code === 0) {
cb();
} else {
cb(Error('Process exited with non-zero exit code: ' + code));
}
}
});
return p;
},
/**
* Starts simulacron process. Uses $SIMULACRON_PATH environemtn variable to determine the
* location of the simulacron jar. If not set $HOME/simulacron.jar is tried instead.
*
* Uses the starting ip range of 127.0.0.101.
*
* @param {Function} cb Callback to be executed when completed, raised with Error if fails.
*/
start: function(cb) {
const self = this;
let simulacronJarPath = process.env['SIMULACRON_PATH'];
if (!simulacronJarPath) {
simulacronJarPath = process.env['HOME'] + "/simulacron.jar";
helper.trace("SIMULACRON_PATH not set, using " + simulacronJarPath);
}
if (!fs.existsSync(simulacronJarPath)) {
throw new Error('Simulacron jar not found at: ' + simulacronJarPath);
}
const processName = 'java';
const params = ['-jar', simulacronJarPath, '--ip', self.startingIp, '-p', this.defaultPort];
let initialized = false;
const timeout = setTimeout(() => cb(new Error('Timed out while waiting for Simulacron server to start.')), 10000);
self.sProcess = self._execute(processName, params, function() {
if(!initialized) {
cb();
}
});
self.sProcess.stdout.on('data', function (data) {
// This is a bit of a kludge, check for a particular log statement which indicates
// that all principals have been created before invoking the completion callback.
if(data.indexOf('Started HTTP server interface') !== -1) {
clearTimeout(timeout);
helper.trace('Simulacron initialized!');
initialized = true;
cb();
}
});
},
/**
* Stops the simulacron process if running.
*
* @param {Function} cb Callback to be executed when completed, raised with Error if fails.
*/
stop: function(cb) {
if(this.sProcess !== undefined) {
if(this.sProcess.exitCode) {
helper.trace('Server already stopped with exit code %d.', this.sProcess.exitCode);
cb();
} else {
if (helper.isWin()) {
const params = ['Stop-Process', this.sProcess.pid];
this._execute('powershell', params, cb);
} else {
this.sProcess.on('close', function () {
cb();
});
this.sProcess.on('error', cb);
this.sProcess.kill('SIGINT');
}
}
} else {
cb(Error('Process is not defined.'));
}
},
/**
* Convenience function for setting up simulacron, starting a simulated cluster and optionally creating a client instance
* that connects to it in before hooks. Cleans up after itself in after hooks.
*
* @param {Array} dcs array of nodes-per-dc configuration (i.e. 2,2,2 creates 3 2 node dcs)
* @param {Object} [options]
* @param {Object} [options.initClient] Determines whether to create a Client instance.
* @param {Object} [options.clientOptions] The options to use to initialize the client.
*/
setup: function (dcs, options) {
options = options || utils.emptyObject;
const clientOptions = options.clientOptions || {};
const simulacronCluster = new SimulacronCluster();
const initClient = options.initClient !== false;
let client;
before((done) => {
this.start(() => simulacronCluster.register(dcs, clientOptions, done));
});
if (initClient) {
const baseOptions = { contactPoints: [this.startingIp], localDataCenter: 'dc1' };
client = new Client(utils.extend({}, options.clientOptions, baseOptions));
before(client.connect.bind(client));
after(client.shutdown.bind(client));
}
afterEach(simulacronCluster.clear.bind(simulacronCluster));
after(simulacronHelper.stop.bind(simulacronHelper));
return { cluster: simulacronCluster, client: client };
},
baseOptions: (function () {
const serverInfo = helper.getServerInfo();
return {
cassandraVersion: helper.getSimulatedCassandraVersion(),
dseVersion: serverInfo.isDse ? serverInfo.version : '',
clusterName: 'testCluster',
activityLog: true,
numTokens: 1
};
})(),
baseAddress: 'localhost',
startingIp: '127.0.0.101',
defaultPort: 8188,
SimulacronCluster: SimulacronCluster
};
function _makeRequest(options, callback) {
const request = http.request(options, function(response) {
// Continuously update stream with data
let body = '';
const statusCode = response.statusCode;
response.on('data', function(d) {
body += d;
});
response.on('end', function() {
if (statusCode >= 400) {
callback(body);
} else if (body === '') {
callback(null, {});
} else {
callback(null, JSON.parse(body));
}
});
});
request.on('error', function(err) {
helper.trace(err.message);
callback(err);
});
return request;
}
/**
* Shared base class for cluster, data center and node types.
*/
function SimulacronTopic() {
this.baseAddress = simulacronHelper.baseAddress;
this.port = simulacronHelper.defaultPort;
}
/**
* @returns {Array} query log entries for the given topic.
*/
SimulacronTopic.prototype.getLogs = function(callback) {
const self = this;
_makeRequest(this._getOptions('log', this.id, 'GET'), function(err, data) {
if (err) {
callback(err);
} else {
callback(err, self._filterLogs(data));
}
}).end();
};
/**
* Clears all query logs for a given topic.
*/
SimulacronTopic.prototype.clearLogs = function(callback) {
_makeRequest(this._getOptions('log', this.id, 'DELETE'), function(err, data) {
callback(err, data);
}).end();
};
/**
* Primes the given query. All nodes associated with this topic will use this prime.
*
* @param {Object} body Body for prime query, refer to simulacron documentation for more details.
* @param {Function} callback
*/
SimulacronTopic.prototype.prime = function(body, callback) {
const request = _makeRequest(this._getOptions('prime', this.id, 'POST'), function(err, data) {
callback(err, data);
});
request.write(JSON.stringify(body));
request.end();
};
/**
* Convenience method that primes the given query string with a simple successful response.
*
* @param {String} query Query string for prime query.
* @param {Function} callback
*/
SimulacronTopic.prototype.primeQuery = function(query, callback) {
this.prime({
when: {
query: query
},
then: {
result: "success"
}
}, callback);
};
/**
* Clear all primes associated with this topic. Also clears primes for underlying members.
*/
SimulacronTopic.prototype.clearPrimes = function(callback) {
_makeRequest(this._getOptions('prime', this.id, 'DELETE'), function(err, data) {
callback(err, data);
}).end();
};
/**
* Clears all primes and activity logs associated with this topic. Also clears data for underlying members.
*/
SimulacronTopic.prototype.clear = function(callback) {
const self = this;
utils.parallel([
self.clearPrimes.bind(self),
self.clearLogs.bind(self)
], callback);
};
/**
* Stops listening for connections and closes existing connections for all associated nodes.
*/
SimulacronTopic.prototype.stop = function(callback) {
const stopNodePath = '/listener/%s?type=stop';
const options = {
host: this.baseAddress,
path: encodeURI(util.format(stopNodePath, this.id)),
port: this.port,
method: 'DELETE'
};
_makeRequest(options, function(err, data) {
callback(err, data);
}).end();
};
/**
* Resume listening for connections for all associated nodes.
*/
SimulacronTopic.prototype.start = function(callback) {
const resumeNodePath = '/listener/%s';
const options = {
host: this.baseAddress,
path: encodeURI(util.format(resumeNodePath, this.id)),
port: this.port,
method: 'PUT'
};
_makeRequest(options, function(err, data) {
callback(err, data);
}).end();
};
SimulacronTopic.prototype.pauseReads = function(callback) {
this._pauseOrResumeReads(true, callback);
};
SimulacronTopic.prototype.resumeReads = function(callback) {
this._pauseOrResumeReads(false, callback);
};
SimulacronTopic.prototype.resumeReadsAsync = util.promisify(SimulacronTopic.prototype.resumeReads);
SimulacronTopic.prototype.pauseReadsAsync = util.promisify(SimulacronTopic.prototype.pauseReads);
SimulacronTopic.prototype._pauseOrResumeReads = function(pause, callback) {
const options = {
host: this.baseAddress,
path: `/pause-reads/${this.id}`,
port: this.port,
method: pause ? 'PUT' : 'DELETE'
};
_makeRequest(options, function(err, data) {
callback(err, data);
}).end();
};
SimulacronTopic.prototype._filterLogs = function(data) {
return data;
};
SimulacronTopic.prototype._getPath = function (endpoint, id) {
const path = '/' + endpoint + '/' + id;
return encodeURI(path);
};
SimulacronTopic.prototype._getOptions = function (endpoint, id, method) {
return {
host: this.baseAddress,
path: this._getPath(endpoint, id),
port: this.port,
method: method,
headers: { 'Content-Type': 'application/json' }
};
};
/**
* Represents a cluster with its data center and node configurations. Use start to initialize.
*/
function SimulacronCluster() {
SimulacronTopic.call(this);
}
util.inherits(SimulacronCluster, SimulacronTopic);
/**
* Registers and starts cluster with given dc configuration and options.
*
* @param {Array} dcs array of nodes-per-dc configuration (i.e. 2,2,2 creates 3 2 node dcs)
* @param {Object} options Startup options
* @param {String} [options.cassandraVersion] Version of cassandra nodes should use. (default is helper.getCassandraVersion())
* @param {String} [options.dseVersion] Version of dse nodes should use. (default is unset)
* @param {Boolean} [options.clusterName] Name of the cluster. (default is 'testCluster')
* @param {Number} [options.numTokens] Number of tokens for each node. (default is 1)
* @param {Function} callback
*/
SimulacronCluster.prototype.register = function(dcs, options, callback) {
const self = this;
const createClusterPath = '/cluster?data_centers=%s&cassandra_version=%s&dse_version=%s&name=%s&activity_log=%s&num_tokens=%d';
options = utils.extend({}, simulacronHelper.baseOptions, options);
if (Array.isArray(dcs)) {
dcs = dcs.join(',');
}
const urlPath = encodeURI(util.format(createClusterPath, dcs, options.cassandraVersion, options.dseVersion,
options.clusterName, options.activityLog, options.numTokens));
const requestOptions = {
host: self.baseAddress,
port: self.port,
path: urlPath,
method: 'POST'
};
_makeRequest(requestOptions, function(err, data) {
if (err) {
return callback(err);
}
self.name = data.name;
self.id = data.id;
self.data = data;
self.dcs = data.data_centers.map(function(dc) {
return new SimulacronDataCenter(self, dc);
});
callback(null, self);
}).end();
};
/**
* Registers and starts cluster with given body.
*
* @param {Object} body payload body.
* @param {Function} callback
*/
SimulacronCluster.prototype.registerWithBody = function(body, callback) {
const self = this;
const requestOptions = {
host: self.baseAddress,
port: self.port,
path: encodeURI('/cluster'),
method: 'POST',
headers: { 'Content-Type': 'application/json' }
};
const request = _makeRequest(requestOptions, function(err, data) {
if (err) {
return callback(err);
}
self.name = data.name;
self.id = data.id;
self.data = data;
self.dcs = data.data_centers.map(function(dc) {
return new SimulacronDataCenter(self, dc);
});
callback(null, self);
});
request.write(JSON.stringify(body));
request.end();
};
/**
* Unregisters and destroys this cluster instance from the server.
*/
SimulacronCluster.prototype.unregister = function(callback) {
_makeRequest(this._getOptions('cluster', this.id, 'DELETE'), function(err, data) {
callback(err, data);
}).end();
};
/**
* Finds a node in the cluster by its id or address.
*
* @param {Number|String} key Identifier of node. If Number, assumes the id of the node in the data center.
* If String, assumes an 'ip:port' designation and looks up node by address.
* @param {Number} [datacenterIndex] Second identifier. If present, 'id' is assumed to be the data center id. If not
* present, 'id' is assumed to be the node id or address. If number is passed in for 'id', 0 is the assumed
* data center id.
* @returns {SimulacronNode} The node, if found.
*/
SimulacronCluster.prototype.node = function(key, datacenterIndex) {
// if the first argument is a string, assume its an address.
let dc;
if (typeof key === "string") {
// iterate over DCs and their nodes looking for first node that matches.
for (let dcIndex = 0; dcIndex < this.dcs.length; dcIndex++) {
dc = this.dcs[dcIndex];
const node = dc.nodes.filter(function (n) {
return n.address === key;
})[0];
if (node) {
return node;
}
}
// node not found, raise error.
throw new Error("No node found for " + key);
}
if (typeof key !== 'number') {
throw new Error('Node key must be either be a String or a Number');
}
// If the dc is not provided, assume first
dc = this.dcs[datacenterIndex || 0];
return dc.node(key);
};
/**
* Finds a data center in the cluster by its id.
*
* @param {Number} id Identifier of the dc.
* @returns {SimulacronDataCenter} The data center, if found.
*/
SimulacronCluster.prototype.dc = function(id) {
return this.dcs[id];
};
/**
* @param {Number} [dataCenterId] The data center to return node addresses from, if not provided assumes 0.
* @returns {Array} Listing of addresses of the nodes in the input dc.
*/
SimulacronCluster.prototype.getContactPoints = function(dataCenterId) {
return this.dcs[dataCenterId || 0].nodes.map(function (node) {
return node.address;
});
};
/**
* Represents a data center with its associated node configurations.
* @param {SimulacronCluster} cluster Parent cluster.
* @param {Object} dc json data from provision response.
*/
function SimulacronDataCenter(cluster, dc) {
SimulacronTopic.call(this);
this.cluster = cluster;
this.data = dc;
this.localId = dc.id;
this.id = cluster.id + '/' + dc.id;
const self = this;
this.nodes = dc.nodes.map(function(node) {
return new SimulacronNode(self, node);
});
}
util.inherits(SimulacronDataCenter, SimulacronTopic);
/**
* Finds a node in the given data center by its id or address.
*
* @param {Number|String} id Identifier of node. If Number, assumes the id of the node in the data center.
* If String, assumes an 'ip:port' designation and looks up node by address.
* @returns {SimulacronNode} The node, if found.
*/
SimulacronDataCenter.prototype.node = function(id) {
// if the first argument is a string, assume its an address.
if (typeof id === "string") {
for (let nodeIndex = 0; nodeIndex < this.nodes.length; nodeIndex++) {
const n = this.nodes[nodeIndex];
if (n.address === id) {
return n;
}
}
// node not found, raise error.
throw new Error("No node found for " + id + " in dc " + this.localId);
}
if (typeof id !== 'number') {
throw new Error('Node id must be a string or a number');
}
return this.nodes[id];
};
/**
* Represents a node.
* @param {SimulacronDataCenter} dc Parent Data Center.
* @param {Object} node json data from provision response.
*/
function SimulacronNode(dc, node) {
SimulacronTopic.call(this);
this.dc = dc;
this.data = node;
this.localId = node.id;
this.id = dc.id + '/' + node.id;
this.address = node.address;
}
util.inherits(SimulacronNode, SimulacronTopic);
SimulacronNode.prototype._filterLogs = function(data) {
return data.data_centers[0].nodes[0].queries;
};
module.exports = simulacronHelper;