| /* |
| * 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; |