UNPKG

msgflo

Version:

Polyglot FBP runtime based on message queues

517 lines (490 loc) 15.9 kB
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); } }); }); };