msgflo
Version:
Polyglot FBP runtime based on message queues
517 lines (490 loc) • 15.9 kB
JavaScript
var Library, addBindings, async, bindingsFromDefinitions, child_process, common, debug, discoverParticipantQueues, fbp, graphBindings, main, normalize, parse, participantCommands, path, pretty, program, queueName, setupBindings, setupParticipants, startProcess, startProcesses, staticGraphBindings, transport,
indexOf = [].indexOf;
common = require('./common');
({Library} = require('./library'));
transport = require('msgflo-nodejs').transport;
fbp = require('fbp');
async = require('async');
debug = require('debug')('msgflo:setup');
child_process = require('child_process');
path = require('path');
program = require('commander');
addBindings = function(broker, bindings, callback) {
var addBinding;
addBinding = function(b, cb) {
return broker.addBinding(b, cb);
};
return async.map(bindings, addBinding, callback);
};
queueName = function(c) {
return common.queueName(c.process, c.port);
};
startProcess = function(cmd, options, callback) {
var args, child, childoptions, env, execCmd, prog, returned;
env = process.env;
env['MSGFLO_BROKER'] = options.broker;
if (options.shell) {
prog = options.shell;
args = ['-c', cmd];
} else {
prog = cmd.split(' ')[0];
args = cmd.split(' ').splice(1);
}
execCmd = prog + ' ' + args.join(' ');
childoptions = {
env: env
};
debug('participant process start', execCmd);
child = child_process.spawn(prog, args, childoptions);
returned = false;
child.on('error', function(err) {
debug('participant error', prog, args.join(' '));
if (returned) {
return;
}
returned = true;
return callback(err, child);
});
// We assume that when somethis is send on stdout, starting is complete
child.stdout.on('data', function(data) {
if (options.forward.indexOf('stdout') !== -1) {
process.stdout.write(data.toString());
}
debug('participant stdout', data.toString());
if (returned) {
return;
}
returned = true;
return callback(null, child);
});
child.stderr.on('data', function(data) {
if (options.forward.indexOf('stderr') !== -1) {
process.stderr.write(data.toString());
}
return debug('participant stderr', data.toString());
});
//return if returned
//returned = true
//return callback new Error data.toString(), child
child.on('exit', function(code, signal) {
debug('child exited', code, signal);
if (returned) {
return;
}
returned = true;
return callback(new Error(`Child '${execCmd}' (pid=${child.pid}) exited with ${code} ${signal}`));
});
return child;
};
participantCommands = function(graph, library, only, ignore) {
var cmd, commands, component, i, iips, isParticipant, len, name, participants;
isParticipant = function(name) {
return common.isParticipant(graph.processes[name]);
};
commands = {};
participants = Object.keys(graph.processes);
if (only != null) {
participants = only;
}
participants = participants.filter(isParticipant);
for (i = 0, len = participants.length; i < len; i++) {
name = participants[i];
if (ignore.indexOf(name) !== -1) {
continue;
}
component = graph.processes[name].component;
iips = common.iipsForRole(graph, name);
cmd = library.componentCommand(component, name, iips);
commands[name] = cmd;
}
return commands;
};
exports.startProcesses = startProcesses = function(commands, options, callback) {
var names, start;
start = (name, cb) => {
var cmd;
cmd = commands[name];
return startProcess(cmd, options, function(err, child) {
if (err) {
return cb(err);
}
return cb(err, {
name: name,
command: cmd,
child: child
});
});
};
debug('starting participants', Object.keys(commands));
names = Object.keys(commands);
return async.map(names, start, function(err, processes) {
var i, len, p, processMap;
if (err) {
return callback(err);
}
processMap = {};
for (i = 0, len = processes.length; i < len; i++) {
p = processes[i];
processMap[p.name] = p.child;
}
return callback(null, processMap);
});
};
exports.killProcesses = function(processes, signal, callback) {
var kill, names, pids;
if (!processes) {
return callback(null);
}
if (!signal) {
signal = 'SIGTERM';
}
kill = function(name, cb) {
var child;
child = processes[name];
if (!child) {
return cb(null);
}
child.once('exit', function(code, signal) {
return cb(null);
});
return child.kill(signal);
};
pids = Object.keys(processes).map(function(n) {
return processes[n].pid;
});
debug('killing participants', pids);
names = Object.keys(processes);
return async.map(names, kill, callback);
};
// Extact the queue bindings, including types from an FBP graph definition
exports.graphBindings = graphBindings = function(graph) {
var binding, bindings, conn, i, isParticipant, len, n, name, process, ref, ref1, ref2, ref3, roundRobinNames, roundRobins;
bindings = [];
roundRobins = {};
ref = graph.processes;
for (name in ref) {
process = ref[name];
if (process.component === 'msgflo/RoundRobin') {
roundRobins[name] = {
type: 'roundrobin'
};
}
}
isParticipant = function(node) {
return common.isParticipant(graph.processes[node]);
};
roundRobinNames = Object.keys(roundRobins);
ref1 = graph.connections;
for (i = 0, len = ref1.length; i < len; i++) {
conn = ref1[i];
if (conn.data) {
// IIP
} else if (ref2 = conn.src.process, indexOf.call(roundRobinNames, ref2) >= 0) {
binding = roundRobins[conn.src.process];
if (conn.src.port === 'deadletter') {
binding.deadletter = queueName(conn.tgt);
} else {
binding.tgt = queueName(conn.tgt);
}
} else if (ref3 = conn.tgt.process, indexOf.call(roundRobinNames, ref3) >= 0) {
binding = roundRobins[conn.tgt.process];
binding.src = queueName(conn.src);
} else if (isParticipant(conn.tgt.process) && isParticipant(conn.src.process)) {
// ordinary connection
bindings.push({
type: 'pubsub',
src: queueName(conn.src),
tgt: queueName(conn.tgt)
});
} else {
debug('no binding for', queueName(conn.src, queueName(conn.tgt)));
}
}
for (n in roundRobins) {
binding = roundRobins[n];
bindings.push(binding);
}
return bindings;
};
staticGraphBindings = function(broker, graph, callback) {
var bindings;
bindings = graphBindings(graph);
return callback(null, bindings);
};
// callbacks with bindings
discoverParticipantQueues = function(broker, graph, options, callback) {
var foundParticipantsByRole;
foundParticipantsByRole = {};
if (typeof broker === 'string') {
broker = transport.getBroker(broker);
}
return broker.connect(function(err) {
var addParticipant, onTimeout, timeout;
if (err) {
return callback(err);
}
addParticipant = function(definition) {
var found, foundRoles, i, idx, len, missingRoles, name, proc, ref, wanted, wantedRoles;
foundParticipantsByRole[definition.role] = definition;
// determine if we are still lacking any definitions
wantedRoles = [];
ref = graph.processes;
for (name in ref) {
proc = ref[name];
wantedRoles.push(name);
}
foundRoles = Object.keys(foundParticipantsByRole);
missingRoles = [];
for (i = 0, len = wantedRoles.length; i < len; i++) {
wanted = wantedRoles[i];
idx = foundRoles.indexOf(wanted);
found = idx !== -1;
if (!found) {
//console.log 'found?', wanted, found
missingRoles.push(wanted);
}
}
debug('addparticipant', definition.role, missingRoles);
//console.log 'wanted, found, missing', wantedRoles, foundRoles, missingRoles
if (missingRoles.length === 0) {
if (!callback) {
return;
}
callback(null, foundParticipantsByRole);
callback = null;
}
};
onTimeout = () => {
if (!callback) {
return;
}
callback(new Error('setup: Participant discovery timed out'));
callback = null;
};
timeout = setTimeout(onTimeout, options.timeout);
return broker.subscribeParticipantChange((msg) => {
var data;
data = msg.data;
if (data.protocol === 'discovery' && data.command === 'participant') {
addParticipant(data.payload);
return broker.ackMessage(msg);
} else {
broker.nackMessage(msg);
throw new Error('Unknown FBP message');
}
});
});
};
bindingsFromDefinitions = function(graph, definitions) {
var bindings, conn, findQueue, i, isParticipant, len, ref, srcDef, srcQueue, tgtDef, tgtQueue;
bindings = [];
isParticipant = function(node) {
return common.isParticipant(graph.processes[node]);
};
findQueue = function(ports, portname) {
var found, i, len, port;
found = null;
for (i = 0, len = ports.length; i < len; i++) {
port = ports[i];
//console.log 'port', port.id, portname, port.id == portname, port.queue
if (port.id === portname) {
found = port.queue;
}
}
if (!found) {
throw new Error(`Could not find ${portname} in ${JSON.stringify(ports)}`);
}
return found;
};
ref = graph.connections;
for (i = 0, len = ref.length; i < len; i++) {
conn = ref[i];
if (!conn.src) { // IIP
continue;
}
if (isParticipant(conn.tgt.process) && isParticipant(conn.src.process)) {
// ordinary connection
srcDef = definitions[conn.src.process];
tgtDef = definitions[conn.tgt.process];
srcQueue = findQueue(srcDef.outports, conn.src.port);
tgtQueue = findQueue(tgtDef.inports, conn.tgt.port);
console;
bindings.push({
type: 'pubsub',
src: srcQueue,
tgt: tgtQueue
});
} else {
debug('no binding for', queueName(conn.src, queueName(conn.tgt)));
}
}
return bindings;
};
exports.normalizeOptions = normalize = function(options) {
common.normalizeOptions(options);
if (!options.libraryfile) {
options.libraryfile = path.join(process.cwd(), 'package.json');
}
if (typeof options.only === 'string' && options.only) {
options.only = options.only.split(',');
}
if (typeof options.ignore === 'string' && options.ignore) {
options.ignore = options.ignore.split(',');
}
if (typeof options.forward === 'string' && options.forward) {
options.forward = options.forward.split(',');
}
if (!options.only) {
options.only = null;
}
if (!options.ignore) {
options.ignore = [];
}
if (!options.forward) {
options.forward = [];
}
if (!options.extrabindings) {
options.extrabindings = [];
}
if (options.discover == null) {
options.discover = false;
}
if (options.forever == null) {
options.forever = false;
}
if (options.timeout == null) {
options.timeout = 10000;
}
return options;
};
exports.bindings = setupBindings = function(options, callback) {
var getBindings;
options = normalize(options);
getBindings = staticGraphBindings; // use queue name convention, read directly from graph file
if (options.discover) {
// wait for FBP discovery messsages, use queues from there
getBindings = function(broker, graph, cb) {
return discoverParticipantQueues(options.broker, graph, options, function(err, defs) {
var bindings;
if (err) {
return cb(err);
}
//console.log 'got defs', definitions
bindings = bindingsFromDefinitions(graph, defs);
return cb(null, bindings);
});
};
}
if (!callback) {
throw new Error('ffss');
}
return common.readGraph(options.graphfile, function(err, graph) {
var broker;
if (err) {
return callback(err);
}
broker = transport.getBroker(options.broker);
return broker.connect(function(err) {
if (err) {
return callback(err);
}
return getBindings(broker, graph, function(err, bindings) {
if (err) {
return callback(err);
}
bindings = bindings.concat(options.extrabindings);
return addBindings(broker, bindings, function(err) {
return callback(err, bindings, graph);
});
});
});
});
};
exports.participants = setupParticipants = function(options, callback) {
options = normalize(options);
return common.readGraph(options.graphfile, function(err, graph) {
var lib;
if (err) {
return callback(err);
}
lib = new Library({
configfile: options.libraryfile,
componentdir: options.componentdir
});
return lib.load(function(err) {
var commands;
if (err) {
return callback(err);
}
commands = participantCommands(graph, lib, options.only, options.ignore);
return startProcesses(commands, options, callback);
});
});
};
exports.parse = parse = function(args) {
var graph;
graph = null;
program.arguments('<graph.fbp/.json>').option('--broker <URL>', 'URL of broker to connect to', String, null).option('--participants [BOOL]', 'Also set up participants, not just bindings', Boolean, false).option('--only <one,two,three>', 'Only set up participants for these roles', String, '').option('--ignore <one,two,three>', 'Do not set up participants for these roles', String, null).option('--library <FILE.json>', 'Library definition to use', String, 'package.json').option('--componentdir <DIR>', 'Directory where components are located', String, '').option('--forward [stderr,stdout]', 'Forward child process stdout and/or stderr', String, '').option('--discover [BOOL]', 'Whether to wait for FBP discovery messages for queue info', Boolean, false).option('--timeout <SECONDS>', 'How long to wait for discovery messages', Number, 30).option('--forever [BOOL]', 'Keep running until killed by signal', Boolean, false).option('--shell [shell]', 'Run participant commands in a shell', String, '').action(function(gr, env) {
return graph = gr;
}).parse(args);
program.libraryfile = program.library;
program.graphfile = graph;
program.timeout = program.timeout * 1000;
return program;
};
exports.prettyFormatBindings = pretty = function(bindings) {
var b, i, len, lines, type;
lines = [];
for (i = 0, len = bindings.length; i < len; i++) {
b = bindings[i];
type = b.type.toUpperCase();
if (b.type === 'roundrobin') {
if (b.tgt && b.deadletter) {
lines.push(`DEADLETTER:\t ${b.tgt} -> ${b.deadletter}`);
}
if (b.tgt && b.src) {
lines.push(`ROUNDROBIN:\t ${b.src} -> ${b.tgt}`);
}
} else if (b.type === 'pubsub') {
lines.push(`PUBSUB: \t ${b.src} -> ${b.tgt}`);
} else {
lines.push(`UNKNOWN binding type: ${b.type}`);
}
}
return lines.join('\n');
};
exports.main = main = function() {
var maybeSetupParticipants, options;
options = parse(process.argv);
if (!options.graphfile) {
console.error('ERROR: No graph file specified');
program.help();
process.exit();
}
maybeSetupParticipants = function(options, callback) {
return callback(null, {});
};
if (options.participants) {
maybeSetupParticipants = setupParticipants;
}
return maybeSetupParticipants(options, function(err, p) {
if (err) {
throw err;
}
console.log('Set up participants', Object.keys(p));
return setupBindings(options, function(err, bindings) {
if (err) {
throw err;
}
console.log('Set up bindings:\n', pretty(bindings));
if (options.forever) {
console.log('--forever enabled, keeping alive');
return setInterval(function() {
return null; // just keep alive
}, 1000);
} else {
return process.exit(0);
}
});
});
};