qlobber-pg
Version:
PostgreSQL-based pub-sub and workqueues. Supports AMQP-like wildcard topics.
492 lines (418 loc) • 16.5 kB
JavaScript
'use strict';
const { EventEmitter } = require('events');
const { randomBytes } = require('crypto');
const { fork } = require('child_process');
const path = require('path');
const { cpus } = require('os');
const { times, each, eachLimit, queue, parallel } = require('async');
const { QlobberPG } = require('..');
let expect;
const { argv } = require('yargs');
const cp_remote = require('cp-remote');
const config = require('config');
const iferr = require('iferr');
const rabbitmq_bindings = require('./fixtures/rabbitmq_bindings.js');
function topic_sort(a, b) {
return parseInt(a.substr(1), 10) - parseInt(b.substr(1), 10);
}
function sum(buf) {
let r = 0;
for (let b of buf) {
r += b;
}
return r;
}
function rabbitmq_tests(name, QCons, num_queues, rounds, msglen, retry_prob, expected, f) {
it('should pass rabbitmq tests (' + name + ', num_queues=' + num_queues + ', rounds=' + rounds + ', msglen=' + msglen + ', retry_prob=' + retry_prob + ')', function (done) {
const timeout = 20 * 60 * 1000;
this.timeout(timeout);
let total = 0;
let expected2 = {};
for (let e of expected) {
total += e[1].length * rounds;
expected2[e[0]] = [];
for (let i = 0; i < rounds; ++i) {
expected2[e[0]] = expected2[e[0]].concat(e[1]);
}
expected2[e[0]].sort(topic_sort);
}
let expected_result_single = [];
for (let i = 0; i < rounds; ++i) {
expected_result_single = expected_result_single.concat(
Object.keys(expected2));
}
expected_result_single.sort();
let subs = [];
let single_sum = 0;
let expected_single_sum = 0;
let result_single = [];
let count_single = 0;
let result = {};
let sums = {};
let expected_sums = {};
let count = 0;
function received(qpgs, n, topic, value, data, single) {
expect(subs[n][value], value).to.be.true;
if (single) {
expect(expected2[topic]).to.contain(value);
single_sum += Buffer.isBuffer(data) ? sum(data) : data;
result_single.push(topic);
++count_single;
} else {
result[topic] = result[topic] || [];
result[topic].push(value);
sums[topic] = sums[topic] || 0;
sums[topic] += Buffer.isBuffer(data) ? sum(data) : data;
++count;
}
//console.log(result);
//console.log(count, total, count_single, expected.length * rounds);
if ((count === total) &&
(count_single === expected.length * rounds)) {
// wait a bit to catch duplicates
return setTimeout(() => {
result_single.sort();
expect(result_single).to.eql(expected_result_single);
expect(single_sum).to.equal(expected_single_sum);
for (let t in result) {
if (Object.prototype.hasOwnProperty.call(result, t)) {
result[t].sort(topic_sort);
}
}
expect(result).to.eql(expected2);
expect(sums).to.eql(expected_sums);
each(qpgs, (qpg, next) => qpg.stop(next), done);
}, 10 * 1000);
}
if (count_single > expected.length * rounds) {
return done(new Error('more single messages than expected'));
}
if (count > total) {
return done(new Error('more messages than expected total'));
}
}
function subscribe(qpgs, n, topic, value, cb) {
const qpg = qpgs[n];
function handler(data, info, cb) {
if (info.single && (Math.random() < retry_prob)) {
return cb('dummy retry');
}
received(qpgs, n, info.topic, value, data, info.single);
cb();
}
qpg.subscribe(topic, handler, iferr(cb, () => {
qpg.__submap = qpg.__submap || {};
qpg.__submap[value] = handler;
cb();
}));
}
function unsubscribe(qpg, topic, value, cb) {
//console.log("UNSUBSCRIBE", qpg._name, topic, value);
if (value) {
//console.log("UNSUBSCRIBE2", qpg._name, topic, value, qpg.__submap[value]);
return qpg.unsubscribe(topic, qpg.__submap[value], iferr(cb, () => {
delete qpg.__submap[value];
cb();
}));
}
qpg.unsubscribe(topic, value, cb);
}
times(num_queues, function (n, cb) {
const qpg = new QCons(Object.assign({
name: `test${n}`
}, config), num_queues, n);
qpg.on('start', iferr(cb, () => {
if ((n === 0) && (QCons === QlobberPG)) {
return qpg._queue.push(cb => {
qpg._client.query('DELETE FROM messages', cb);
}, err => cb(err, qpg));
}
cb(null, qpg);
}));
}, iferr(done, qpgs => {
function publish() {
const pq = queue((task, cb) => {
const buf = randomBytes(msglen);
const s = sum(buf);
expected_sums[task] = expected_sums[task] || 0;
for (let i = 0; i < expected2[task].length / rounds; ++i) {
expected_sums[task] += s;
}
expected_single_sum += s;
parallel([cb => {
qpgs[Math.floor(Math.random() * num_queues)].publish(
task,
buf,
{ ttl: timeout },
cb);
}, cb => {
qpgs[Math.floor(Math.random() * num_queues)].publish(
task,
buf,
{ ttl: timeout, single: true },
cb);
}], cb);
}, num_queues * 5);
for (let i = 0; i < rounds; ++i) {
for (let e of expected) {
pq.push(e[0], iferr(done, () => {}));
}
}
if (Object.keys(subs).length === 0) {
pq.drain(() => {
setTimeout(() => {
expect(count).to.equal(0);
expect(count_single).to.equal(0);
each(qpgs, (qpg, next) => qpg.stop(next), done);
}, 10 * 1000);
});
}
}
let assigned = {};
const q = queue((i, cb) => {
const n = Math.floor(Math.random() * num_queues);
const entry = rabbitmq_bindings.test_bindings[i];
subs[n] = subs[n] || {};
subs[n][entry[1]] = true;
assigned[i] = n;
assigned[entry[0]] = assigned[entry[0]] || [];
assigned[entry[0]].push({ n, v: entry[1] });
subscribe(qpgs, n, entry[0], entry[1], cb);
}, num_queues * 5);
for (let i = 0; i < rabbitmq_bindings.test_bindings.length; ++i) {
q.push(i, iferr(done, () => {}));
}
q.drain(() => {
if (f) {
f(qpgs, subs, assigned, unsubscribe, iferr(done, publish));
} else {
publish();
}
});
}));
});
}
function rabbitmq(prefix, QCons, queues, rounds, msglen, retry_prob) {
prefix += ', ';
rabbitmq_tests(prefix + 'before remove', QCons, queues, rounds, msglen, retry_prob, rabbitmq_bindings.expected_results_before_remove);
rabbitmq_tests(prefix + 'after remove', QCons, queues, rounds, msglen, retry_prob, rabbitmq_bindings.expected_results_after_remove, (qpgs, subs, assigned, unsubscribe, cb) => {
eachLimit(rabbitmq_bindings.bindings_to_remove, qpgs.length * 5, (i, next) => {
const n = assigned[i - 1];
const v = rabbitmq_bindings.test_bindings[i - 1][1];
unsubscribe(qpgs[n], rabbitmq_bindings.test_bindings[i - 1][0], v, iferr(next, () => {
assigned[i - 1] = null;
subs[n][v] = null;
next();
}));
}, cb);
});
rabbitmq_tests(prefix + 'after remove_all', QCons, queues, rounds, msglen, retry_prob, rabbitmq_bindings.expected_results_after_remove_all, (qpgs, subs, assigned, unsubscribe, cb) => {
eachLimit(rabbitmq_bindings.bindings_to_remove, qpgs.length * 5, (i, next) => {
const topic = rabbitmq_bindings.test_bindings[i - 1][0];
eachLimit(assigned[topic], qpgs.length * 5, (nv, next2) => {
unsubscribe(qpgs[nv.n], topic, nv.v, iferr(next, () => {
subs[nv.n][nv.v] = null;
next2();
}));
}, iferr(next, () => {
assigned[i - 1] = null;
assigned[topic] = [];
next();
}));
}, cb);
});
rabbitmq_tests(prefix + 'after clear', QCons, queues, rounds, msglen, retry_prob, rabbitmq_bindings.expected_results_after_clear, (qpgs, subs, assigned, unsubscribe, cb) => {
each(qpgs, (qpg, next) => {
unsubscribe(qpg, undefined, undefined, next);
}, iferr(cb, () => {
subs.length = 0;
cb();
}));
});
}
function rabbitmq2(prefix, QCons, queues, rounds, msglen) {
rabbitmq(prefix, QCons, queues, rounds, msglen, 0);
rabbitmq(prefix, QCons, queues, rounds, msglen, 0.5);
}
function rabbitmq3(prefix, QCons, queues, rounds) {
rabbitmq2(prefix, QCons, queues, rounds, 1);
rabbitmq2(prefix, QCons, queues, rounds, 25 * 1024);
}
function rabbitmq4(prefix, QCons, queues) {
rabbitmq3(prefix, QCons, queues, 1);
rabbitmq3(prefix, QCons, queues, 50);
}
class MPQPGBase extends EventEmitter {
constructor(child, options) {
super();
this._name = options.name;
this._handlers = {};
this._handler_count = 0;
this._pub_cbs = {};
this._pub_cb_count = 0;
this._sub_cbs = {};
this._sub_cb_count = 0;
this._unsub_cbs = {};
this._unsub_cb_count = 0;
this._topics = {};
this._send_queue = queue((msg, cb) => child.send(msg, cb));
child.on('error', err => this.emit('error', err));
child.on('exit', () => this.emit('stop'));
child.on('message', msg => {
//console.log("RECEIVED MESSAGE FROM CHILD", options.index, msg);
if (msg.type === 'start') {
this.emit('start', msg.err);
} else if (msg.type === 'stop') {
this._send_queue.push({ type: 'exit' });
} else if (msg.type === 'received') {
this._handlers[msg.handler](msg.sum, msg.info, err => {
this._send_queue.push({
type: 'recv_callback',
cb: msg.cb,
err
});
});
} else if (msg.type === 'sub_callback') {
const cb = this._sub_cbs[msg.cb];
delete this._sub_cbs[msg.cb];
cb();
} else if (msg.type === 'unsub_callback') {
const cb = this._unsub_cbs[msg.cb];
delete this._unsub_cbs[msg.cb];
cb();
} else if (msg.type === 'pub_callback') {
const cb = this._pub_cbs[msg.cb];
delete this._pub_cbs[msg.cb];
cb(msg.err);
}
});
}
subscribe(topic, handler, cb) {
this._handlers[this._handler_count] = handler;
handler.__count = this._handler_count;
this._sub_cbs[this._sub_cb_count] = cb;
this._topics[topic] = this._topics[topic] || {};
this._topics[topic][this._handler_count] = true;
this._send_queue.push({
type: 'subscribe',
topic,
handler: this._handler_count,
cb: this._sub_cb_count
});
++this._handler_count;
++this._sub_cb_count;
}
unsubscribe(topic, handler, cb) {
if (topic === undefined) {
this._unsub_cbs[this._unsub_cb_count] = () => {
this._handlers = {};
this._topics = {};
cb();
};
this._send_queue.push({
type: 'unsubscribe',
cb: this._unsub_cb_count
});
++this._unsub_cb_count;
} else if (handler === undefined) {
let n = this._topics[topic].length;
for (let h of this._topics[topic]) {
this._unsub_cbs[this._unsub_cb_count] = () => {
delete this._handlers[h];
if (--n === 0) {
delete this._topics[topic];
cb();
}
};
this._send_queue.push({
type: 'unsubscribe',
topic,
handler: h,
cb: this._unsub_cb_count
});
++this._unsub_cb_count;
}
} else {
this._unsub_cbs[this._unsub_cb_count] = () => {
delete this._handlers[handler.__count];
cb();
};
this._send_queue.push({
type: 'unsubscribe',
topic,
handler: handler.__count,
cb: this._unsub_cb_count
});
++this._unsub_cb_count;
}
}
publish(topic, payload, options, cb) {
this._pub_cbs[this._pub_cb_count] = cb;
this._send_queue.push({
type: 'publish',
topic,
payload: payload.toString('base64'),
options,
cb: this._pub_cb_count
});
++this._pub_cb_count;
}
stop(cb) {
this._send_queue.push({
type: 'stop'
});
if (cb) {
this.once('stop', cb);
}
}
}
class MPQPG extends MPQPGBase {
constructor(options, total, index) {
options = Object.assign({ total, index }, options);
super(
fork(
path.join(__dirname, 'fixtures', 'mpqpg.js'),
[Buffer.from(JSON.stringify(options)).toString('hex')]),
options);
}
}
function make_RemoteMPQPG(hosts) {
return class extends MPQPGBase {
constructor(options, total, index) {
options = Object.assign({ total, index }, options);
super(
cp_remote.run(
hosts[index],
path.join(__dirname, 'fixtures', 'mpqpg.js'),
Buffer.from(JSON.stringify(options)).toString('hex')),
options);
this._host = hosts[index];
}
};
}
describe('rabbit', function () {
before(async () => {
({ expect } = await import('chai'));
});
if (argv.remote) {
let hosts;
if (argv.remote === true) {
hosts = ['localhost', 'localhost'];
} else if (typeof argv.remote === 'string') {
hosts = [argv.remote];
} else if (argv.remote[0] === true) {
hosts = argv.remote.slice(1);
} else {
hosts = argv.remote;
}
rabbitmq4('distributed', make_RemoteMPQPG(hosts), hosts.length);
} else if (argv.multi) {
rabbitmq4('multi-process', MPQPG, argv.queues || cpus().length);
} else {
rabbitmq4('single-process', QlobberPG, 1);
rabbitmq4('single-process', QlobberPG, 2);
if (!process.env.NODE_V8_COVERAGE && !process.env.APPVEYOR) {
rabbitmq4('single-process', QlobberPG, 5);
}
}
});