msgflo
Version:
Polyglot FBP runtime based on message queues
267 lines (216 loc) • 7.79 kB
text/coffeescript
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: (, ) ->
= {}
= {} # connId -> { queue: opt String, handler: opt function }
= {} # iipId -> value
= false
= new ParticipantManager .address
'participant',
start: (callback) ->
.connect (err) =>
debug 'connected', err
return callback err if err
.subscribeParticipantChange (msg) =>
msg.data
.ackMessage msg
= true
debug 'started', err,
return callback null
stop: (callback) ->
= false
.disconnect (err) =>
return callback err if err
if
.stop callback
else
return callback null
handleFbpMessage: (data) ->
if data.protocol == 'discovery' and data.command == 'participant'
data.payload
else
throw new Error 'Unknown FBP message'
addParticipant: (definition) ->
debug 'addParticipant', definition.id
[definition.id] = definition
'participant-added', definition
'participant', 'added', definition
removeParticipant: (id) ->
definition = [id]
'participant-removed', definition
'participant', 'removed', definition
sendTo: (participantId, inport, message) ->
debug 'sendTo', participantId, inport, message
part = [participantId]
port = findPort part, 'inport', inport
.sendToQueue port.queue, message, (err) ->
throw err if err
subscribeTo: (participantId, outport, handler) ->
part = [participantId]
debug 'subscribeTo', participantId, outport
# console.log part.outports, outport
port = findPort part, 'outport', outport
ackHandler = (msg) =>
return if not
handler msg
.ackMessage msg
.subscribeToQueue port.queue, ackHandler, (err) ->
throw err if err
unsubscribeFrom: () -> # FIXME: implement
connect: (fromId, fromPort, toId, toName) ->
findQueue = (partId, dir, portName) =>
part = [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
.bindQueue edge.srcQueue, edge.tgtQueue, (err) =>
id = connId fromId, fromPort, toId, toName
[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
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 [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]
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]
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
[id] = data
partId, portId, data if
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
graph.processes[id] =
component: part.component
for id, conn of
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
graph, callback
participantsByRole: (role) ->
matchRole = (id) =>
part = [id]
return part.role == role
m = Object.keys().filter matchRole
return m
loadGraph: (graph, callback) ->
# TODO: clear existing state?
# Waiting until all participants have registerd
waitForParticipant = (processId, callback) =>
existing = processId
return callback null, [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
'participant-added', onParticipantAdded
return callback null
'participant-added', onParticipantAdded
# Connecting edges
connectEdge = (edge, callback) =>
src = edge.src.process
tgt = edge.tgt.process
src, edge.src.port, tgt, edge.tgt.port
return callback null
# Sending IIPs
sendInitial = (iip, callback) =>
tgt = iip.tgt.process
tgt[0], iip.tgt.port, iip.data
return callback null
async.map Object.keys(graph.processes), waitForParticipant, (err) =>
= 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'
= (err != null)
return callback err if err
return callback null
# For testing, start participants
.graph = graph
.start (err) ->
throw err if err
exports.Coordinator = Coordinator