@getanthill/datastore
Version:
Event-Sourced Datastore
430 lines (357 loc) • 10.4 kB
JavaScript
process.env.TELEMETRY_LOGGER_LEVEL = 'critical';
const fs = require('node:fs');
const { default: services } = require('../dist/services');
const { default: App } = require('../dist/App');
const { Datastore, Aggregator } = require('../dist/sdk');
const memwatch = require('@airbnb/node-memwatch');
const DEFAULT_TIMES_PATH = '/tmp/times.csv';
const ModelConfig = {
is_enabled: true,
db: 'datastore',
name: 'things',
correlation_field: 'thing_id',
retry_duration: 30000,
encrypted_fields: ['firstname'],
schema: {
model: {
properties: {
firstname: {
type: 'string',
},
count: {
type: 'number',
},
},
},
events: {
CREATED: {
'0_0_0': {
properties: {
firstname: {
type: 'string',
},
count: {
type: 'number',
},
},
},
},
UPDATED: {
'0_0_0': {
properties: {
firstname: {
type: 'string',
},
count: {
type: 'number',
},
},
},
},
RESTORED: {
'0_0_0': {
additionalProperties: true,
},
},
ROLLBACKED: {
'0_0_0': {
additionalProperties: true,
},
},
PATCHED: {
'0_0_0': {
additionalProperties: true,
},
},
},
},
};
function uuid() {
return 'uuid' + (Math.random() * 1e16).toFixed(0);
}
function time(from) {
const hrTime = process.hrtime(from);
return (hrTime[0] * 1000000000 + hrTime[1]) / 1000000;
}
function average(arr) {
return Math.round((arr.reduce((a, b) => a + b, 0) / arr.length) * 100) / 100;
}
function median(values) {
if (values.length === 0) throw new Error('No inputs');
values.sort(function (a, b) {
return a - b;
});
var half = Math.floor(values.length / 2);
if (values.length % 2) return values[half];
return (values[half - 1] + values[half]) / 2.0;
}
async function flow(app, sdk, i, steps) {
const _uuid = uuid();
let entity = _entity;
if (steps.withCreation === true) {
i === -1 && console.debug('[bench#flow] With entity creation');
const { data: __entity } = await sdk.create('projections', {
firstname: `alice:${_uuid}`,
});
entity = __entity;
steps.op += 1;
}
if (steps.withDecrypt === true) {
i === -1 && console.debug('[bench#flow] With entity decrypt');
const {
data: [decryptedEntity],
} = await sdk.decrypt('projections', [entity]);
steps.op += 1;
}
if (steps.withUpdate === true) {
i === -1 && console.debug('[bench#flow] With entity update');
const { data: updated } = await sdk.update(
'projections',
entity.projection_id,
{
count: Math.random(),
},
);
steps.op += 1;
}
if (steps.withGet === true) {
i === -1 && console.debug('[bench#flow] With entity get');
const { data: got } = await sdk.get('projections', entity.projection_id);
steps.op += 1;
}
if (steps.withFind === true) {
i === -1 && console.debug('[bench#flow] With entity find');
const { data: found } = await sdk.find('projections', {
firstname: `alice:${_uuid}`,
_must_hash: true, // <-- Required for index
});
steps.op += 1;
}
if (steps.withAggregation === true) {
i === -1 && console.debug('[bench#flow] With entity aggregation');
// const aggregator = new Aggregator(new Map([['datastore', sdk]]));
const { data } = await sdk.aggregate(
[
{
type: 'map',
},
{
type: 'fetch',
datastore: 'datastore',
model: 'projections',
source: 'events',
destination: 'events',
query: {
projection_id: entity.projection_id,
},
},
],
{
entity,
},
);
steps.op += 1;
}
}
function printProgress(progress) {
process.stdout.clearLine(0);
process.stdout.cursorTo(0);
process.stdout.write(progress + '%');
}
let _uuid;
let _entity;
async function main(
filePath = DEFAULT_TIMES_PATH,
threshold = 5,
iterations = 100,
parallel = 10,
) {
threshold = parseFloat(threshold);
iterations = Number.parseInt(iterations, 10);
parallel = Number.parseInt(parallel, 10);
const steps = {
withCreation: process.argv.includes('--create'),
withDecrypt: process.argv.includes('--decrypt'),
withUpdate: process.argv.includes('--update'),
withGet: process.argv.includes('--get'),
withFind: process.argv.includes('--find'),
withAggregation: process.argv.includes('--aggregate'),
};
const nbOperations = Object.values(steps).reduce(
(s, c) => s + (c === true ? 1 : 0),
0,
);
console.log('[bench] Configuration', {
threshold,
iterations,
parallel,
steps,
});
services.config.mode = 'development';
services.config.security.tokens = [
{
id: 'admin',
level: 'admin',
token: 'token',
},
];
services.config.features.api.admin = true;
services.config.features.api.aggregate = steps.withAggregation;
services.config.features.api.openAPI.isEnabled = true;
services.config.features.api.updateSpecOnModelsChange = true;
services.config.features.initInternalModels = true;
services.config.security.activeNumberEncryptionKeys = 3;
services.config.security.encryptionKeys = {
all: [
'0171568cb939a6a678f80a2a5204cec8',
'0171568cb939a6a678f80a2a5204cec9',
'0171568cb939a6a678f80a2a5204ceca',
],
};
services.config.features.amqp.isEnabled = false;
const dbName = `datastore_${(Math.random() * 1e10).toFixed(0)}`;
services.config.mongodb.databases[0].url = `mongodb://localhost:27017/${dbName}`;
services.config.mongodb.databases[1].url = `mongodb://localhost:27017/${dbName}`;
services.config.datastores = [
{
name: 'datastore',
baseUrl: `http://127.0.0.1:${services.config.port}`,
timeout: 30_000,
token: 'token',
debug: false,
connector: 'http',
},
];
const app = new App(services);
const sdk = new Datastore({
baseUrl: `http://localhost:${app.services.config.port}`,
debug: false,
token: 'token',
timeout: 300000,
});
await app.start();
const modelConfig = {
...ModelConfig,
name: 'projections',
correlation_field: 'projection_id',
encrypted_fields: ['firstname'],
indexes: [
{
collection: 'projections',
fields: { 'firstname::hash': 1 },
opts: { name: 'email_hash_1' },
},
],
};
await sdk.createModel(modelConfig);
await sdk.createModelIndexes(modelConfig);
// Global entity
_uuid = uuid();
const res = await sdk.create('projections', {
firstname: `alice:${_uuid}`,
});
_entity = res.data;
await flow(app, sdk, -1, steps);
await app.restart();
const fd = fs.createWriteStream(filePath);
const hd = new memwatch.HeapDiff();
const start = process.hrtime();
let total = 0;
let totalRatio = 0;
let lastRatio = 1;
let min = Infinity;
for (let i = 0; i < iterations; i++) {
const tic = process.hrtime();
await Promise.all(
new Array(parallel).fill(1).map(() => flow(app, sdk, i, steps)),
);
const tac = time(tic);
total += tac;
min = Math.min(min, tac);
totalRatio += tac / min;
const newRatio = totalRatio / (i + 1);
fd.write(
`${i}, ${tac}, ${totalRatio / (i + 1)}, ${newRatio - lastRatio}\n`,
);
lastRatio = newRatio;
const timePerStep = total / i;
printProgress((100 * i) / iterations);
process.stdout.clearLine(0);
process.stdout.cursorTo(0);
process.stdout.write(
`[bench] progress=${((100 * i) / iterations).toFixed(
2,
)}% op=${nbOperations} eta=${(
((iterations - i) * timePerStep) /
1000
).toFixed(0)}s ${tac.toFixed(3)}ms/it ${(tac / parallel).toFixed(
3,
)}ms/req (x${(tac / min).toFixed(2)}) ${(
tac /
parallel /
nbOperations
).toFixed(3)}ms/op`,
);
}
const stop = time(start);
const rows = fs
.readFileSync(filePath)
.toString()
.split('\n')
.filter((str) => !!str);
const times = rows.map((str) => parseFloat(str.split(', ')[1]));
const ratio = rows.map((str) => parseFloat(str.split(', ')[2]));
process.stdout.clearLine(0);
process.stdout.cursorTo(0);
minIt = Math.min(...times);
medianIt = median(times);
averageIt = average(times);
maxIt = Math.max(...times);
console.table([
{
i: iterations,
it: iterations,
op: nbOperations,
ratio: Math.floor(ratio[[iterations - 1]] * 100) / 100,
min_it: Math.floor(minIt * 100) / 100,
med_it: Math.floor(medianIt * 100) / 100,
avg_it: Math.floor(averageIt * 100) / 100,
max_it: Math.floor(maxIt * 100) / 100,
min_req: Math.floor((minIt / parallel) * 100) / 100,
med_req: Math.floor((medianIt / parallel) * 100) / 100,
avg_req: Math.floor((averageIt / parallel) * 100) / 100,
max_req: Math.floor((maxIt / parallel) * 100) / 100,
min_op: Math.floor((minIt / parallel / nbOperations) * 100) / 100,
med_op: Math.floor((medianIt / parallel / nbOperations) * 100) / 100,
avg_op: Math.floor((averageIt / parallel / nbOperations) * 100) / 100,
max_op: Math.floor((maxIt / parallel / nbOperations) * 100) / 100,
},
]);
process.stdout.write('[bench] Generating memory heap diff...');
const diff = hd.end();
process.stdout.clearLine(0);
process.stdout.cursorTo(0);
console.log('[bench] memory:', diff.change.size);
console.log('[bench] duration:', `${Math.floor(stop / 1000)}s`);
await app.stop();
fs.writeFileSync('/tmp/memory.json', JSON.stringify(diff));
if (Math.abs(ratio[iterations - 1]) > 1.5) {
throw new Error(`[bench] 💥 Time response is increasing`);
}
if (diff.change.size_bytes / 1024 / 1024 > threshold) {
throw new Error(
`[bench] 💥 Memory threshold exceeded (${diff.change.size} > ${threshold} mb)`,
);
}
}
main(...process.argv.slice(2))
.then(() => {
console.log('[bench] Ended successfully');
})
.catch((err) => {
console.error(err);
if (err?.response?.data) {
console.error('[bench] Error', err?.response?.data);
}
process.exit(1);
});