UNPKG

msgflo

Version:

Polyglot FBP runtime based on message queues

267 lines (216 loc) 7.79 kB
debug = require('debug')('msgflo:coordinator') EventEmitter = require('events').EventEmitter fs = require 'fs' async = require 'async' ParticipantManager = require('./manager').ParticipantManager findPort = (def, type, portName) -> ports = if type == 'inport' then def.inports else def.outports for port in ports return port if port.id == portName return null connId = (fromId, fromPort, toId, toPort) -> return "#{fromId} #{fromPort} -> #{toPort} #{toId}" fromConnId = (id) -> t = id.split ' ' return [ t[0], t[1], t[4], t[3] ] iipId = (part, port) -> return "#{part} #{port}" fromIipId = (id) -> return id.split ' ' class Coordinator extends EventEmitter constructor: (@broker, @initialGraph) -> @participants = {} @connections = {} # connId -> { queue: opt String, handler: opt function } @iips = {} # iipId -> value @started = false @manager = new ParticipantManager @broker.address @on 'participant', @checkParticipantConnections start: (callback) -> @broker.connect (err) => debug 'connected', err return callback err if err @broker.subscribeParticipantChange (msg) => @handleFbpMessage msg.data @broker.ackMessage msg @started = true debug 'started', err, @started return callback null stop: (callback) -> @started = false @broker.disconnect (err) => return callback err if err if @manager @manager.stop callback else return callback null handleFbpMessage: (data) -> if data.protocol == 'discovery' and data.command == 'participant' @addParticipant data.payload else throw new Error 'Unknown FBP message' addParticipant: (definition) -> debug 'addParticipant', definition.id @participants[definition.id] = definition @emit 'participant-added', definition @emit 'participant', 'added', definition removeParticipant: (id) -> definition = @participants[id] @emit 'participant-removed', definition @emit 'participant', 'removed', definition sendTo: (participantId, inport, message) -> debug 'sendTo', participantId, inport, message part = @participants[participantId] port = findPort part, 'inport', inport @broker.sendToQueue port.queue, message, (err) -> throw err if err subscribeTo: (participantId, outport, handler) -> part = @participants[participantId] debug 'subscribeTo', participantId, outport # console.log part.outports, outport port = findPort part, 'outport', outport ackHandler = (msg) => return if not @started handler msg @broker.ackMessage msg @broker.subscribeToQueue port.queue, ackHandler, (err) -> throw err if err unsubscribeFrom: () -> # FIXME: implement connect: (fromId, fromPort, toId, toName) -> findQueue = (partId, dir, portName) => part = @participants[partId] for port in part[dir] return port.queue if port.id == portName edge = fromId: fromId fromPort: fromPort toId: toId toName: toName srcQueue: findQueue fromId, 'outports', fromPort tgtQueue: findQueue toId, 'inports', toName @broker.bindQueue edge.srcQueue, edge.tgtQueue, (err) => id = connId fromId, fromPort, toId, toName @connections[id] = edge # TODO: introduce some "spying functionality" to provide edge messages, add tests disconnect: (fromId, fromPortId, toId, toPortId) -> # FIXME: implement checkParticipantConnections: (action, participant) -> findConnectedPorts = (dir, srcPort) => conn = [] # return conn if not srcPort.queue for id, part of @participants for port in part[dir] continue if not port.queue conn.push { part: part, port: port } if port.queue == srcPort.queue return conn isConnected = (e) => [fromId, fromPort, toId, toPort] = e id = connId fromId, fromPort, toId, toPort return @connections[id]? if action == 'added' id = participant.id # inbound for port in participant.inports matches = findConnectedPorts 'outports', port for m in matches e = [m.part.id, m.port.id, id, port.id] @connect e[0], e[1], e[2], e[3] if not isConnected e # outbound for port in participant.outports matches = findConnectedPorts 'inports', port for m in matches e = [id, port.id, m.part.id, m.port.id] @connect e[0], e[1], e[2], e[3] if not isConnected e else if action == 'removed' # TODO: implement else # ignored addInitial: (partId, portId, data) -> id = iipId partId, portId @iips[id] = data @sendTo partId, portId, data if @started removeInitial: (partId, portId) -> # FIXME: implement # Do we need to remove it from the queue?? serializeGraph: (name) -> graph = properties: name: name processes: {} connections: [] inports: [] outports: [] for id, part of @participants graph.processes[id] = component: part.component for id, conn of @connections parts = fromConnId id edge = src: process: parts[0] port: parts[1] tgt: process: parts[2] port: parts[3] graph.connections.push edge return graph loadGraphFile: (path, callback) -> fs.readFile path, {encoding:'utf-8'}, (err, contents) => return callback err if err try graph = JSON.parse contents catch e return callback e if e @loadGraph graph, callback participantsByRole: (role) -> matchRole = (id) => part = @participants[id] return part.role == role m = Object.keys(@participants).filter matchRole return m loadGraph: (graph, callback) -> # TODO: clear existing state? # Waiting until all participants have registerd waitForParticipant = (processId, callback) => existing = @participantsByRole processId return callback null, @participants[existing[0]] if existing.length onTimeout = () => return callback new Error 'Participant discovery timeout' timeout = setTimeout onTimeout, 10000 onParticipantAdded = (part) => if part.role == processId debug 'onParticipant', part.role # FIXME: take into account multiple participants with same role clearTimeout timeout @removeListener 'participant-added', onParticipantAdded return callback null @on 'participant-added', onParticipantAdded # Connecting edges connectEdge = (edge, callback) => src = @participantsByRole edge.src.process tgt = @participantsByRole edge.tgt.process @connect src, edge.src.port, tgt, edge.tgt.port return callback null # Sending IIPs sendInitial = (iip, callback) => tgt = @participantsByRole iip.tgt.process @addInitial tgt[0], iip.tgt.port, iip.data return callback null async.map Object.keys(graph.processes), waitForParticipant, (err) => @started = err != null debug 'participants loaded', err return callback err if err edges = [] iips = [] for conn in graph.connections target = if conn.src then edges else iips target.push conn async.map edges, connectEdge, (err) => debug 'edges connected', err return callback err if err async.map iips, sendInitial, (err) => debug 'IIPs sent' @started = (err != null) return callback err if err return callback null # For testing, start participants @manager.graph = graph @manager.start (err) -> throw err if err exports.Coordinator = Coordinator