Skip to content

Added simple stress test #241

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
May 23, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions gulpfile.babel.js
Original file line number Diff line number Diff line change
Expand Up @@ -242,3 +242,11 @@ gulp.task('stop-neo4j', function (done) {
sharedNeo4j.stop(neo4jHome);
done();
});

gulp.task('run-stress-tests', function () {
return gulp.src('test/**/stress.test.js')
.pipe(jasmine({
includeStackTrace: true,
verbose: true
}));
});
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@
"build": "gulp all",
"start-neo4j": "gulp start-neo4j",
"stop-neo4j": "gulp stop-neo4j",
"run-stress-tests": "gulp run-stress-tests",
"run-tck": "gulp run-tck",
"docs": "esdoc -c esdoc.json",
"versionRelease": "gulp set --version $VERSION && npm version $VERSION --no-git-tag-version"
},
"main": "lib/index.js",
"devDependencies": {
"async": "^2.4.0",
"babel-core": "^6.17.0",
"babel-plugin-transform-runtime": "^6.15.0",
"babel-preset-es2015": "^6.16.0",
Expand Down Expand Up @@ -49,6 +51,7 @@
"gulp-util": "^3.0.6",
"gulp-watch": "^4.3.5",
"jasmine-reporters": "^2.0.7",
"lodash": "^4.17.4",
"lolex": "^1.5.2",
"merge-stream": "^1.0.0",
"minimist": "^1.2.0",
Expand Down
337 changes: 337 additions & 0 deletions test/v1/stress.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,337 @@
/**
* Copyright (c) 2002-2017 "Neo Technology,","
* Network Engine for Objects in Lund AB [http://neotechnology.com]
*
* This file is part of Neo4j.
*
* 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.
*/

import neo4j from '../../src/v1';
import {READ, WRITE} from '../../src/v1/driver';
import parallelLimit from 'async/parallelLimit';
import _ from 'lodash';
import sharedNeo4j from '../internal/shared-neo4j';

describe('stress tests', () => {

const TEST_MODES = {
fast: {
commandsCount: 5000,
parallelism: 8,
maxRunTimeMs: 120000 // 2 minutes
},
extended: {
commandsCount: 2000000,
parallelism: 16,
maxRunTimeMs: 3600000 // 60 minutes
}
};

const READ_QUERY = 'MATCH (n) RETURN n LIMIT 1';
const WRITE_QUERY = 'CREATE (person:Person:Employee {name: {name}, salary: {salary}}) RETURN person';

const TEST_MODE = modeFromEnvOrDefault('STRESS_TEST_MODE');
const DATABASE_URI = fromEnvOrDefault('STRESS_TEST_DATABASE_URI', 'bolt://localhost');
const LOGGING_ENABLED = fromEnvOrDefault('STRESS_TEST_LOGGING_ENABLED', false);

let originalJasmineTimeout;
let driver;

beforeEach(done => {
originalJasmineTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL;
jasmine.DEFAULT_TIMEOUT_INTERVAL = TEST_MODE.maxRunTimeMs;

driver = neo4j.driver(DATABASE_URI, sharedNeo4j.authToken);

cleanupDb(driver).then(() => done());
});

afterEach(done => {
jasmine.DEFAULT_TIMEOUT_INTERVAL = originalJasmineTimeout;

cleanupDb(driver).then(() => {
driver.close();
done();
});
});

it('basic', done => {
const context = new Context(driver, LOGGING_ENABLED);
const commands = createCommands(context);

console.time('Basic-stress-test');
parallelLimit(commands, TEST_MODE.parallelism, error => {
console.timeEnd('Basic-stress-test');

if (error) {
done.fail(error);
}

verifyServers(context);
verifyNodeCount(context)
.then(() => done())
.catch(error => done.fail(error));
});
});

function createCommands(context) {
const uniqueCommands = createUniqueCommands(context);

const commands = [];
for (let i = 0; i < TEST_MODE.commandsCount; i++) {
const randomCommand = _.sample(uniqueCommands);
commands.push(randomCommand);
}

console.log(`Generated ${TEST_MODE.commandsCount} commands`);

return commands;
}

function createUniqueCommands(context) {
return [
readQueryCommand(context),
readQueryWithBookmarkCommand(context),
readQueryInTxCommand(context),
readQueryInTxWithBookmarkCommand(context),
writeQueryCommand(context),
writeQueryWithBookmarkCommand(context),
writeQueryInTxCommand(context),
writeQueryInTxWithBookmarkCommand(context)
];
}

function readQueryCommand(context) {
return queryCommand(context, READ_QUERY, () => noParams(), READ, false);
}

function readQueryWithBookmarkCommand(context) {
return queryCommand(context, READ_QUERY, () => noParams(), READ, true);
}

function readQueryInTxCommand(context) {
return queryInTxCommand(context, READ_QUERY, () => noParams(), READ, false);
}

function readQueryInTxWithBookmarkCommand(context) {
return queryInTxCommand(context, READ_QUERY, () => noParams(), READ, true);
}

function writeQueryCommand(context) {
return queryCommand(context, WRITE_QUERY, () => randomParams(), WRITE, false);
}

function writeQueryWithBookmarkCommand(context) {
return queryCommand(context, WRITE_QUERY, () => randomParams(), WRITE, true);
}

function writeQueryInTxCommand(context) {
return queryInTxCommand(context, WRITE_QUERY, () => randomParams(), WRITE, false);
}

function writeQueryInTxWithBookmarkCommand(context) {
return queryInTxCommand(context, WRITE_QUERY, () => randomParams(), WRITE, true);
}

function queryCommand(context, query, paramsSupplier, accessMode, useBookmark) {
return callback => {
const commandId = context.nextCommandId();
const session = newSession(context, accessMode, useBookmark);
const params = paramsSupplier();

context.log(commandId, `About to run ${accessMode} query`);

session.run(query, params).then(result => {
context.queryCompleted(result, accessMode);
context.log(commandId, `Query completed successfully`);

session.close(() => {
const possibleError = verifyQueryResult(result);
callback(possibleError);
});
}).catch(error => {
context.log(commandId, `Query failed with error ${JSON.stringify(error)}`);
callback(error);
});
};
}

function queryInTxCommand(context, query, paramsSupplier, accessMode, useBookmark) {
return callback => {
const commandId = context.nextCommandId();
const session = newSession(context, accessMode, useBookmark);
const tx = session.beginTransaction();
const params = paramsSupplier();

context.log(commandId, `About to run ${accessMode} query in TX`);

tx.run(query, params).then(result => {
let commandError = verifyQueryResult(result);

tx.commit().catch(commitError => {
context.log(commandId, `Transaction commit failed with error ${JSON.stringify(error)}`);
if (!commandError) {
commandError = commitError;
}
}).then(() => {
context.queryCompleted(result, accessMode, session.lastBookmark());
context.log(commandId, `Transaction committed successfully`);

session.close(() => {
callback(commandError);
});
});

}).catch(error => {
context.log(commandId, `Query failed with error ${JSON.stringify(error)}`);
callback(error);
});
};
}

function verifyQueryResult(result) {
if (!result) {
return new Error(`Received undefined result`);
} else if (result.records.length === 0) {
// it is ok to receive no nodes back for read queries at the beginning of the test
return null;
} else if (result.records.length === 1) {
const record = result.records[0];
return verifyRecord(record);
} else {
return new Error(`Unexpected amount of records received: ${JSON.stringify(result)}`);
}
}

function verifyRecord(record) {
const node = record.get(0);

if (!_.isEqual(['Person', 'Employee'], node.labels)) {
return new Error(`Unexpected labels in node: ${JSON.stringify(node)}`);
}

const propertyKeys = _.keys(node.properties);
if (!_.isEmpty(propertyKeys) && !_.isEqual(['name', 'salary'], propertyKeys)) {
return new Error(`Unexpected property keys in node: ${JSON.stringify(node)}`);
}

return null;
}

function verifyNodeCount(context) {
const expectedNodeCount = context.createdNodesCount;

const session = context.driver.session();
return session.run('MATCH (n) RETURN count(n)').then(result => {
const record = result.records[0];
const count = record.get(0).toNumber();

if (count !== expectedNodeCount) {
throw new Error(`Unexpected node count: ${count}, expected: ${expectedNodeCount}`);
}
});
}

function verifyServers(context) {
const routing = DATABASE_URI.indexOf('bolt+routing') === 0;
const seenServers = context.seenServers();

if (routing && seenServers.length <= 1) {
throw new Error(`Routing driver used too few servers: ${seenServers}`);
} else if (!routing && seenServers.length !== 1) {
throw new Error(`Direct driver used too many servers: ${seenServers}`);
}
}

function randomParams() {
return {
name: `Person-${Date.now()}`,
salary: Date.now()
};
}

function noParams() {
return {};
}

function newSession(context, accessMode, useBookmark) {
if (useBookmark) {
return context.driver.session(accessMode, context.bookmark);
}
return context.driver.session(accessMode);
}

function modeFromEnvOrDefault(envVariableName) {
const modeName = fromEnvOrDefault(envVariableName, 'fast');
const mode = TEST_MODES[modeName];
if (!mode) {
throw new Error(`Unknown test mode: ${modeName}`);
}
console.log(`Selected '${modeName}' mode for the stress test`);
return mode;
}

function fromEnvOrDefault(envVariableName, defaultValue) {
if (process && process.env && process.env[envVariableName]) {
return process.env[envVariableName];
}
return defaultValue;
}

function cleanupDb(driver) {
const session = driver.session();
return session.run('MATCH (n) DETACH DELETE n').then(() => {
session.close();
}).catch(error => {
console.log('Error clearing the database: ', error);
});
}

class Context {

constructor(driver, loggingEnabled) {
this.driver = driver;
this.bookmark = null;
this.createdNodesCount = 0;
this._commandIdCouter = 0;
this._loggingEnabled = loggingEnabled;
this._seenServers = new Set();
}

queryCompleted(result, accessMode, bookmark) {
if (accessMode === WRITE) {
this.createdNodesCount++;
}
if (bookmark) {
this.bookmark = bookmark;
}
this._seenServers.add(result.summary.server.address);
}

nextCommandId() {
return this._commandIdCouter++;
}

seenServers() {
return Array.from(this._seenServers);
}

log(commandId, message) {
if (this._loggingEnabled) {
console.log(`Command [${commandId}]: ${message}`);
}
}
}

});