Skip to content

Commit ed03624

Browse files
authored
Merge pull request #241 from lutovich/1.3-stress-it
Added simple stress test
2 parents 1ab1b8b + a541121 commit ed03624

File tree

3 files changed

+348
-0
lines changed

3 files changed

+348
-0
lines changed

gulpfile.babel.js

+8
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

+3
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

+337
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,337 @@
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+
verifyServers(context);
82+
verifyNodeCount(context)
83+
.then(() => done())
84+
.catch(error => done.fail(error));
85+
});
86+
});
87+
88+
function createCommands(context) {
89+
const uniqueCommands = createUniqueCommands(context);
90+
91+
const commands = [];
92+
for (let i = 0; i < TEST_MODE.commandsCount; i++) {
93+
const randomCommand = _.sample(uniqueCommands);
94+
commands.push(randomCommand);
95+
}
96+
97+
console.log(`Generated ${TEST_MODE.commandsCount} commands`);
98+
99+
return commands;
100+
}
101+
102+
function createUniqueCommands(context) {
103+
return [
104+
readQueryCommand(context),
105+
readQueryWithBookmarkCommand(context),
106+
readQueryInTxCommand(context),
107+
readQueryInTxWithBookmarkCommand(context),
108+
writeQueryCommand(context),
109+
writeQueryWithBookmarkCommand(context),
110+
writeQueryInTxCommand(context),
111+
writeQueryInTxWithBookmarkCommand(context)
112+
];
113+
}
114+
115+
function readQueryCommand(context) {
116+
return queryCommand(context, READ_QUERY, () => noParams(), READ, false);
117+
}
118+
119+
function readQueryWithBookmarkCommand(context) {
120+
return queryCommand(context, READ_QUERY, () => noParams(), READ, true);
121+
}
122+
123+
function readQueryInTxCommand(context) {
124+
return queryInTxCommand(context, READ_QUERY, () => noParams(), READ, false);
125+
}
126+
127+
function readQueryInTxWithBookmarkCommand(context) {
128+
return queryInTxCommand(context, READ_QUERY, () => noParams(), READ, true);
129+
}
130+
131+
function writeQueryCommand(context) {
132+
return queryCommand(context, WRITE_QUERY, () => randomParams(), WRITE, false);
133+
}
134+
135+
function writeQueryWithBookmarkCommand(context) {
136+
return queryCommand(context, WRITE_QUERY, () => randomParams(), WRITE, true);
137+
}
138+
139+
function writeQueryInTxCommand(context) {
140+
return queryInTxCommand(context, WRITE_QUERY, () => randomParams(), WRITE, false);
141+
}
142+
143+
function writeQueryInTxWithBookmarkCommand(context) {
144+
return queryInTxCommand(context, WRITE_QUERY, () => randomParams(), WRITE, true);
145+
}
146+
147+
function queryCommand(context, query, paramsSupplier, accessMode, useBookmark) {
148+
return callback => {
149+
const commandId = context.nextCommandId();
150+
const session = newSession(context, accessMode, useBookmark);
151+
const params = paramsSupplier();
152+
153+
context.log(commandId, `About to run ${accessMode} query`);
154+
155+
session.run(query, params).then(result => {
156+
context.queryCompleted(result, accessMode);
157+
context.log(commandId, `Query completed successfully`);
158+
159+
session.close(() => {
160+
const possibleError = verifyQueryResult(result);
161+
callback(possibleError);
162+
});
163+
}).catch(error => {
164+
context.log(commandId, `Query failed with error ${JSON.stringify(error)}`);
165+
callback(error);
166+
});
167+
};
168+
}
169+
170+
function queryInTxCommand(context, query, paramsSupplier, accessMode, useBookmark) {
171+
return callback => {
172+
const commandId = context.nextCommandId();
173+
const session = newSession(context, accessMode, useBookmark);
174+
const tx = session.beginTransaction();
175+
const params = paramsSupplier();
176+
177+
context.log(commandId, `About to run ${accessMode} query in TX`);
178+
179+
tx.run(query, params).then(result => {
180+
let commandError = verifyQueryResult(result);
181+
182+
tx.commit().catch(commitError => {
183+
context.log(commandId, `Transaction commit failed with error ${JSON.stringify(error)}`);
184+
if (!commandError) {
185+
commandError = commitError;
186+
}
187+
}).then(() => {
188+
context.queryCompleted(result, accessMode, session.lastBookmark());
189+
context.log(commandId, `Transaction committed successfully`);
190+
191+
session.close(() => {
192+
callback(commandError);
193+
});
194+
});
195+
196+
}).catch(error => {
197+
context.log(commandId, `Query failed with error ${JSON.stringify(error)}`);
198+
callback(error);
199+
});
200+
};
201+
}
202+
203+
function verifyQueryResult(result) {
204+
if (!result) {
205+
return new Error(`Received undefined result`);
206+
} else if (result.records.length === 0) {
207+
// it is ok to receive no nodes back for read queries at the beginning of the test
208+
return null;
209+
} else if (result.records.length === 1) {
210+
const record = result.records[0];
211+
return verifyRecord(record);
212+
} else {
213+
return new Error(`Unexpected amount of records received: ${JSON.stringify(result)}`);
214+
}
215+
}
216+
217+
function verifyRecord(record) {
218+
const node = record.get(0);
219+
220+
if (!_.isEqual(['Person', 'Employee'], node.labels)) {
221+
return new Error(`Unexpected labels in node: ${JSON.stringify(node)}`);
222+
}
223+
224+
const propertyKeys = _.keys(node.properties);
225+
if (!_.isEmpty(propertyKeys) && !_.isEqual(['name', 'salary'], propertyKeys)) {
226+
return new Error(`Unexpected property keys in node: ${JSON.stringify(node)}`);
227+
}
228+
229+
return null;
230+
}
231+
232+
function verifyNodeCount(context) {
233+
const expectedNodeCount = context.createdNodesCount;
234+
235+
const session = context.driver.session();
236+
return session.run('MATCH (n) RETURN count(n)').then(result => {
237+
const record = result.records[0];
238+
const count = record.get(0).toNumber();
239+
240+
if (count !== expectedNodeCount) {
241+
throw new Error(`Unexpected node count: ${count}, expected: ${expectedNodeCount}`);
242+
}
243+
});
244+
}
245+
246+
function verifyServers(context) {
247+
const routing = DATABASE_URI.indexOf('bolt+routing') === 0;
248+
const seenServers = context.seenServers();
249+
250+
if (routing && seenServers.length <= 1) {
251+
throw new Error(`Routing driver used too few servers: ${seenServers}`);
252+
} else if (!routing && seenServers.length !== 1) {
253+
throw new Error(`Direct driver used too many servers: ${seenServers}`);
254+
}
255+
}
256+
257+
function randomParams() {
258+
return {
259+
name: `Person-${Date.now()}`,
260+
salary: Date.now()
261+
};
262+
}
263+
264+
function noParams() {
265+
return {};
266+
}
267+
268+
function newSession(context, accessMode, useBookmark) {
269+
if (useBookmark) {
270+
return context.driver.session(accessMode, context.bookmark);
271+
}
272+
return context.driver.session(accessMode);
273+
}
274+
275+
function modeFromEnvOrDefault(envVariableName) {
276+
const modeName = fromEnvOrDefault(envVariableName, 'fast');
277+
const mode = TEST_MODES[modeName];
278+
if (!mode) {
279+
throw new Error(`Unknown test mode: ${modeName}`);
280+
}
281+
console.log(`Selected '${modeName}' mode for the stress test`);
282+
return mode;
283+
}
284+
285+
function fromEnvOrDefault(envVariableName, defaultValue) {
286+
if (process && process.env && process.env[envVariableName]) {
287+
return process.env[envVariableName];
288+
}
289+
return defaultValue;
290+
}
291+
292+
function cleanupDb(driver) {
293+
const session = driver.session();
294+
return session.run('MATCH (n) DETACH DELETE n').then(() => {
295+
session.close();
296+
}).catch(error => {
297+
console.log('Error clearing the database: ', error);
298+
});
299+
}
300+
301+
class Context {
302+
303+
constructor(driver, loggingEnabled) {
304+
this.driver = driver;
305+
this.bookmark = null;
306+
this.createdNodesCount = 0;
307+
this._commandIdCouter = 0;
308+
this._loggingEnabled = loggingEnabled;
309+
this._seenServers = new Set();
310+
}
311+
312+
queryCompleted(result, accessMode, bookmark) {
313+
if (accessMode === WRITE) {
314+
this.createdNodesCount++;
315+
}
316+
if (bookmark) {
317+
this.bookmark = bookmark;
318+
}
319+
this._seenServers.add(result.summary.server.address);
320+
}
321+
322+
nextCommandId() {
323+
return this._commandIdCouter++;
324+
}
325+
326+
seenServers() {
327+
return Array.from(this._seenServers);
328+
}
329+
330+
log(commandId, message) {
331+
if (this._loggingEnabled) {
332+
console.log(`Command [${commandId}]: ${message}`);
333+
}
334+
}
335+
}
336+
337+
});

0 commit comments

Comments
 (0)