Skip to content

Commit b8d3441

Browse files
committed
Added simple stress test
It executed read and write queries randomly against a given or default (`bolt://localhost`) URI. Queries are executed in both explicit and implicit transactions, both with and without bookmarks. Test can read environment variables to switch mode (fast or extended), turn on/off logging and change database URI. This makes it possible to run test against real cluster using `bolt+routing` scheme. New npm script is added for this `run-stress-tests`
1 parent 1ab1b8b commit b8d3441

File tree

3 files changed

+336
-0
lines changed

3 files changed

+336
-0
lines changed

gulpfile.babel.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,3 +242,11 @@ gulp.task('stop-neo4j', function (done) {
242242
sharedNeo4j.stop(neo4jHome);
243243
done();
244244
});
245+
246+
gulp.task('run-stress-tests', function () {
247+
return gulp.src('test/**/stress.test.js')
248+
.pipe(jasmine({
249+
includeStackTrace: true,
250+
verbose: true
251+
}));
252+
});

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,14 @@
1414
"build": "gulp all",
1515
"start-neo4j": "gulp start-neo4j",
1616
"stop-neo4j": "gulp stop-neo4j",
17+
"run-stress-tests": "gulp run-stress-tests",
1718
"run-tck": "gulp run-tck",
1819
"docs": "esdoc -c esdoc.json",
1920
"versionRelease": "gulp set --version $VERSION && npm version $VERSION --no-git-tag-version"
2021
},
2122
"main": "lib/index.js",
2223
"devDependencies": {
24+
"async": "^2.4.0",
2325
"babel-core": "^6.17.0",
2426
"babel-plugin-transform-runtime": "^6.15.0",
2527
"babel-preset-es2015": "^6.16.0",
@@ -49,6 +51,7 @@
4951
"gulp-util": "^3.0.6",
5052
"gulp-watch": "^4.3.5",
5153
"jasmine-reporters": "^2.0.7",
54+
"lodash": "^4.17.4",
5255
"lolex": "^1.5.2",
5356
"merge-stream": "^1.0.0",
5457
"minimist": "^1.2.0",

test/v1/stress.test.js

Lines changed: 325 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,325 @@
1+
/**
2+
* Copyright (c) 2002-2017 "Neo Technology,","
3+
* Network Engine for Objects in Lund AB [http://neotechnology.com]
4+
*
5+
* This file is part of Neo4j.
6+
*
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
*/
19+
20+
import neo4j from '../../src/v1';
21+
import {READ, WRITE} from '../../src/v1/driver';
22+
import parallelLimit from 'async/parallelLimit';
23+
import _ from 'lodash';
24+
import sharedNeo4j from '../internal/shared-neo4j';
25+
26+
describe('stress tests', () => {
27+
28+
const TEST_MODES = {
29+
fast: {
30+
commandsCount: 5000,
31+
parallelism: 8,
32+
maxRunTimeMs: 120000 // 2 minutes
33+
},
34+
extended: {
35+
commandsCount: 2000000,
36+
parallelism: 16,
37+
maxRunTimeMs: 3600000 // 60 minutes
38+
}
39+
};
40+
41+
const READ_QUERY = 'MATCH (n) RETURN n LIMIT 1';
42+
const WRITE_QUERY = 'CREATE (person:Person:Employee {name: {name}, salary: {salary}}) RETURN person';
43+
44+
const TEST_MODE = modeFromEnvOrDefault('STRESS_TEST_MODE');
45+
const DATABASE_URI = fromEnvOrDefault('STRESS_TEST_DATABASE_URI', 'bolt://localhost');
46+
const LOGGING_ENABLED = fromEnvOrDefault('STRESS_TEST_LOGGING_ENABLED', false);
47+
48+
let originalJasmineTimeout;
49+
let driver;
50+
51+
beforeEach(done => {
52+
originalJasmineTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL;
53+
jasmine.DEFAULT_TIMEOUT_INTERVAL = TEST_MODE.maxRunTimeMs;
54+
55+
driver = neo4j.driver(DATABASE_URI, sharedNeo4j.authToken);
56+
57+
cleanupDb(driver).then(() => done());
58+
});
59+
60+
afterEach(done => {
61+
jasmine.DEFAULT_TIMEOUT_INTERVAL = originalJasmineTimeout;
62+
63+
cleanupDb(driver).then(() => {
64+
driver.close();
65+
done();
66+
});
67+
});
68+
69+
it('basic', done => {
70+
const context = new Context(driver, LOGGING_ENABLED);
71+
const commands = createCommands(context);
72+
73+
console.time('Basic-stress-test');
74+
parallelLimit(commands, TEST_MODE.parallelism, error => {
75+
console.timeEnd('Basic-stress-test');
76+
77+
if (error) {
78+
done.fail(error);
79+
}
80+
81+
verifyNodeCount(context)
82+
.then(() => done())
83+
.catch(error => done.fail(error));
84+
});
85+
});
86+
87+
function createCommands(context) {
88+
const uniqueCommands = createUniqueCommands(context);
89+
90+
const commands = [];
91+
for (let i = 0; i < TEST_MODE.commandsCount; i++) {
92+
const randomCommand = _.sample(uniqueCommands);
93+
commands.push(randomCommand);
94+
}
95+
96+
console.log(`Generated ${TEST_MODE.commandsCount} commands`);
97+
98+
return commands;
99+
}
100+
101+
function createUniqueCommands(context) {
102+
return [
103+
readQueryCommand(context),
104+
readQueryWithBookmarkCommand(context),
105+
readQueryInTxCommand(context),
106+
readQueryInTxWithBookmarkCommand(context),
107+
writeQueryCommand(context),
108+
writeQueryWithBookmarkCommand(context),
109+
writeQueryInTxCommand(context),
110+
writeQueryInTxWithBookmarkCommand(context)
111+
];
112+
}
113+
114+
function readQueryCommand(context) {
115+
return queryCommand(context, READ_QUERY, () => noParams(), READ, false);
116+
}
117+
118+
function readQueryWithBookmarkCommand(context) {
119+
return queryCommand(context, READ_QUERY, () => noParams(), READ, true);
120+
}
121+
122+
function readQueryInTxCommand(context) {
123+
return queryInTxCommand(context, READ_QUERY, () => noParams(), READ, false);
124+
}
125+
126+
function readQueryInTxWithBookmarkCommand(context) {
127+
return queryInTxCommand(context, READ_QUERY, () => noParams(), READ, true);
128+
}
129+
130+
function writeQueryCommand(context) {
131+
return queryCommand(context, WRITE_QUERY, () => randomParams(), WRITE, false);
132+
}
133+
134+
function writeQueryWithBookmarkCommand(context) {
135+
return queryCommand(context, WRITE_QUERY, () => randomParams(), WRITE, true);
136+
}
137+
138+
function writeQueryInTxCommand(context) {
139+
return queryInTxCommand(context, WRITE_QUERY, () => randomParams(), WRITE, false);
140+
}
141+
142+
function writeQueryInTxWithBookmarkCommand(context) {
143+
return queryInTxCommand(context, WRITE_QUERY, () => randomParams(), WRITE, true);
144+
}
145+
146+
function queryCommand(context, query, paramsSupplier, accessMode, useBookmark) {
147+
return callback => {
148+
const commandId = context.nextCommandId();
149+
const session = newSession(context, accessMode, useBookmark);
150+
const params = paramsSupplier();
151+
152+
context.log(commandId, `About to run ${accessMode} query`);
153+
154+
session.run(query, params).then(result => {
155+
156+
if (accessMode === WRITE) {
157+
context.nodeCreated();
158+
}
159+
160+
context.log(commandId, `Query completed successfully`);
161+
session.close(() => {
162+
const possibleError = verifyQueryResult(result);
163+
callback(possibleError);
164+
});
165+
}).catch(error => {
166+
context.log(commandId, `Query failed with error ${JSON.stringify(error)}`);
167+
callback(error);
168+
});
169+
};
170+
}
171+
172+
function queryInTxCommand(context, query, paramsSupplier, accessMode, useBookmark) {
173+
return callback => {
174+
const commandId = context.nextCommandId();
175+
const session = newSession(context, accessMode, useBookmark);
176+
const tx = session.beginTransaction();
177+
const params = paramsSupplier();
178+
179+
context.log(commandId, `About to run ${accessMode} query in TX`);
180+
181+
tx.run(query, params).then(result => {
182+
let commandError = verifyQueryResult(result);
183+
184+
tx.commit().catch(commitError => {
185+
context.log(commandId, `Transaction commit failed with error ${JSON.stringify(error)}`);
186+
if (!commandError) {
187+
commandError = commitError;
188+
}
189+
}).then(() => {
190+
context.setBookmark(session.lastBookmark());
191+
192+
context.log(commandId, `Transaction committed successfully`);
193+
194+
if (accessMode === WRITE) {
195+
context.nodeCreated();
196+
}
197+
session.close(() => {
198+
callback(commandError);
199+
});
200+
});
201+
202+
}).catch(error => {
203+
context.log(commandId, `Query failed with error ${JSON.stringify(error)}`);
204+
callback(error);
205+
});
206+
};
207+
}
208+
209+
function verifyQueryResult(result) {
210+
if (!result) {
211+
return new Error(`Received undefined result`);
212+
} else if (result.records.length === 0) {
213+
// it is ok to receive no nodes back for read queries at the beginning of the test
214+
return null;
215+
} else if (result.records.length === 1) {
216+
const record = result.records[0];
217+
return verifyRecord(record);
218+
} else {
219+
return new Error(`Unexpected amount of records received: ${JSON.stringify(result)}`);
220+
}
221+
}
222+
223+
function verifyRecord(record) {
224+
const node = record.get(0);
225+
226+
if (!_.isEqual(['Person', 'Employee'], node.labels)) {
227+
return new Error(`Unexpected labels in node: ${JSON.stringify(node)}`);
228+
}
229+
230+
const propertyKeys = _.keys(node.properties);
231+
if (!_.isEmpty(propertyKeys) && !_.isEqual(['name', 'salary'], propertyKeys)) {
232+
return new Error(`Unexpected property keys in node: ${JSON.stringify(node)}`);
233+
}
234+
235+
return null;
236+
}
237+
238+
function verifyNodeCount(context) {
239+
const expectedNodeCount = context.createdNodesCount;
240+
241+
const session = context.driver.session();
242+
return session.run('MATCH (n) RETURN count(n)').then(result => {
243+
const record = result.records[0];
244+
const count = record.get(0).toNumber();
245+
246+
if (count !== expectedNodeCount) {
247+
throw new Error(`Unexpected node count: ${count}, expected: ${expectedNodeCount}`);
248+
}
249+
});
250+
}
251+
252+
function randomParams() {
253+
return {
254+
name: `Person-${Date.now()}`,
255+
salary: Date.now()
256+
};
257+
}
258+
259+
function noParams() {
260+
return {};
261+
}
262+
263+
function newSession(context, accessMode, useBookmark) {
264+
if (useBookmark) {
265+
return context.driver.session(accessMode, context.bookmark);
266+
}
267+
return context.driver.session(accessMode);
268+
}
269+
270+
function modeFromEnvOrDefault(envVariableName) {
271+
const modeName = fromEnvOrDefault(envVariableName, 'fast');
272+
const mode = TEST_MODES[modeName];
273+
if (!mode) {
274+
throw new Error(`Unknown test mode: ${modeName}`);
275+
}
276+
console.log(`Selected '${modeName}' mode for the stress test`);
277+
return mode;
278+
}
279+
280+
function fromEnvOrDefault(envVariableName, defaultValue) {
281+
if (process && process.env && process.env[envVariableName]) {
282+
return process.env[envVariableName];
283+
}
284+
return defaultValue;
285+
}
286+
287+
function cleanupDb(driver) {
288+
const session = driver.session();
289+
return session.run('MATCH (n) DETACH DELETE n').then(() => {
290+
session.close();
291+
}).catch(error => {
292+
console.log('Error clearing the database: ', error);
293+
});
294+
}
295+
296+
class Context {
297+
298+
constructor(driver, loggingEnabled) {
299+
this.driver = driver;
300+
this.bookmark = null;
301+
this.createdNodesCount = 0;
302+
this._commandIdCouter = 0;
303+
this._loggingEnabled = loggingEnabled;
304+
}
305+
306+
setBookmark(bookmark) {
307+
this.bookmark = bookmark;
308+
}
309+
310+
nodeCreated() {
311+
this.createdNodesCount++;
312+
}
313+
314+
nextCommandId() {
315+
return this._commandIdCouter++;
316+
}
317+
318+
log(commandId, message) {
319+
if (this._loggingEnabled) {
320+
console.log(`Command [${commandId}]: ${message}`);
321+
}
322+
}
323+
}
324+
325+
});

0 commit comments

Comments
 (0)