glutenfree
Version:
A profiler/loganalyzer for nginx/Cetrea Aw.
277 lines (227 loc) • 7.68 kB
text/coffeescript
#!/usr/bin/env coffee
dgram = require("dgram")
optimist = require("optimist")
winston = require("winston")
http = require("http")
uuid = require("node-uuid")
util = require("util")
_ = require("underscore")
fs = require("fs")
cluster = require("cluster")
argv = optimist
.usage("""
Usage: $0 -i [inputfile] -s [server] -p [port] -u [user] -w [password] -n [workers] -t [time] -h
The Profiler (Pr) can operate in two modes; master and slave.
The master-mode is entered by providing an input file (the statistics structure outputted from a LogAnalyzer then ArgumentGenerator w glutenfree targeting).
The slave-mode is entered into by default (when no parameters are given).
""")
.alias("i", "input")
.describe("i","Input file")
.describe("s","The hostname of the server (Aw) to connect to.")
.alias("s", "server")
.describe("p","The port of the server (Aw) to connect to.")
.alias("p", "port")
.describe("u","Username (used in basic-auth)")
.alias("u", "user")
.describe("w","Password (used in basic-auth)")
.alias("w", "password")
.alias("h","help")
.describe("t","Time to wait between requests (in ms)")
.alias("t", "time")
.describe("h","Display usage.")
.alias("n","workers")
.describe("n","Number of workers per slave. Default is number of CPUs on slave machine. Set value to force higher concurrency.")
.argv
if argv.h?
optimist.showHelp()
process.exit(0)
mode = if argv.i? then "master" else "slave"
slaves = {}
if mode is "master"
winston.info "Pr in MASTER mode"
if not (argv.s? and argv.p? and argv.u? and argv.w?)
optimist.showHelp()
winston.error "Missing server, port, user or password. Please provide all four."
process.exit(0)
file = if argv.i[0] is "/" then argv.i else "./#{argv.i}"
input = require(file)
# setup control interface
port = Math.floor((Math.random()*1000)+3000);
ios = require('socket.io').listen(port, {log: false})
winston.info "control interface running on port #{port}"
# io.set('log level', 0)
ios.sockets.on(
"connection",
(socket) ->
socket.on(
"enslave",
(slave) ->
# add slave if it isnt alreadt registered
if not slaves[socket.id]?
winston.info "#{slave.id} enslaved"
# remember slave
slaves[socket.id] = _.extend(slave, { socket: socket })
# tell slave to start profiling
socket.emit(
"profile",
{
targeting: input
server: argv.s
port: argv.p
user: argv.u
password: argv.w
workerCount: argv.n
pause: argv.t
}
)
)
socket.on(
"report"
(hits) ->
slave = slaves[socket.id]
winston.debug "sample", hits[0]
# build csv
csv = _.reduce(
hits
(memo, hit) ->
memo += _.map(
_.values(hit)
(value) ->
if typeof(value) is "string" and value.indexOf(",") > 0 then "\"#{value}\""
else value
)
.join(",") + "\n"
""
)
# appending to file
fs.appendFile(
"#{slave.id}.csv"
csv
(err) ->
if err? then winston.error "Could not write results to file.", err
else winston.info "Results from #{slave.id} appended to file."
)
)
socket.on(
"disconnect",
->
slave = slaves[socket.id]
delete slaves[socket.id]
winston.info "slave #{slave.id} freed"
)
)
# advertise me in loop
setInterval(
() ->
message = new Buffer("GLUTENFREE|Pr(MASTER)|ONLINE|#{port}")
client = dgram.createSocket("udp4")
client.send(
message,
0,
message.length,
5354,
"224.0.0.251",
(err, bytes) ->
client.close()
)
5000
)
else # mode is SLAVE
ioc = require("socket.io-client")
winston.info "Pr in SLAVE mode"
me =
id: uuid.v4()
state: "AVAILABLE"
#224.0.0.251:5353
lsocket = dgram.createSocket("udp4")
iosock = {}
reports = []
# reporting will contain the interval for reporting
reporting = {}
lsocket.on(
"message",
(msg, rinfo) ->
if me.state is "AVAILABLE"
[protocol,type,state,interfaceport] = "#{msg}".split("|")
# reporting function
# reports back to master over iosocket (websocket)
report = (iosock, interval) ->
if reports? and reports.length > 0
payload = _.flatten(_.pluck(reports, "hits"))
#winston.debug "#{reports.length} reports sent to MASTER"
iosock.emit("report", payload)
# reset reports
reports = []
if me.state is "SLAVED" and iosock
setTimeout(
->
report(iosock, interval)
interval
)
iosock = ioc.connect("http://#{rinfo.address}:#{interfaceport}")
iosock.emit("enslave", me)
iosock.on(
"connect",
->
me.state = "SLAVED"
winston.info "enslaved"
)
iosock.on(
"profile",
(order) ->
# extract targeting information
targeting = order.targeting
# setup cluster, ProfilerWorker.coffee contains worker-code
cluster.setupMaster({
exec: "#{__dirname}/ProfilerWorker"
})
# determine number of workers and fork
workerCount = order.workerCount || require("os").cpus().length
for w in [1..workerCount]
do (w) ->
cluster.fork()
cluster.on("exit", (worker) ->
if me.state is "SLAVED"
winston.warn "worker (#{worker?.process?.pid}) died - forking new"
cluster.fork()
)
# send target lookup to worker
cluster.on("online", (worker) ->
# listen for reports from worker
worker.on("message", (msg) ->
switch msg.subject
when "report"
rprt = msg.report
#winston.info "recv report from #{worker.id} containing #{rprt.hits.length} hits"
reports.push(rprt)
)
# wait a tick before ordering new worker around
setTimeout(
->
winston.info "sending targeting to", worker.id
worker.send({ subject: "targeting", clusterId: me.id, server: order.server, port: order.port, user: order.user, password: order.password, targeting: targeting, pause: order.pause })
1000
)
)
# setup timed reporting (every 10th second)
report(iosock, 10000)
)
# go back to AVAILABLE when disconnected
iosock.on(
"disconnect",
->
me.state = "AVAILABLE"
winston.info "freed, now asking workers to die"
cluster.disconnect()
)
)
lsocket.on(
"listening",
() ->
address = lsocket.address()
multicastadr = "224.0.0.251"
winston.info "Pr SLAVE listening for MASTERs at #{address.address}:#{address.port} (multicast at #{multicastadr})"
lsocket.addMembership(multicastadr)
)
# discovery lsocket is bound
lsocket.bind(5354)