guv
Version:
Grid Utilization Virgilante
163 lines (135 loc) • 4.83 kB
text/coffeescript
# guv - Scaling governor of cloud workers
# (c) 2015 The Grid
# guv may be freely distributed under the MIT license
debug = require('debug')('guv:governor')
{ EventEmitter } = require 'events'
bluebird = require 'bluebird'
heroku = require './heroku'
rabbitmq = require './rabbitmq'
scale = require './scale'
# NOTE: original order
extractHistory = (history, rolename, key) ->
predictions = []
for state in history
predictions.push state[rolename][key]
return predictions
locateConfigEntry = (cfg, appName, processName) ->
for name, value of cfg
return name if (appName == value.app && processName == (value.worker || name))
nextState = (cfg, window, queues, queueDetails, currentWorkers) ->
workersObject = {}
for w in currentWorkers
# Here w.role is the heroku process name. We need to map
# based on app, process name to the key in the config (as they are
# not necessarily one and the same.)
configKey = locateConfigEntry(cfg, w.app, w.role)
workersObject[configKey] = w
state = {}
# TODO: store timestamps?
for name, role of cfg
continue if name == '*'
state[name] = s = {}
s.current_jobs = queues[role.queue]
s.metric = role.metric
s.app = role.app
details = queueDetails[role.queue]
if details?
s.consumers = details.consumers
s.drainrate = details.drainrate
s.fillrate = details.fillrate
if not s.current_jobs?
s.error = new Error "Could not get data for queue: #{role.queue}"
else
history = extractHistory window, name, 'estimated_workers'
currentWorkers = # Heroku
s.previous_workers = workersObject[name].quantity
workers = scale.scaleWithHistory role, name, history, currentWorkers, s.current_jobs
s.estimated_workers = workers.estimate
if workers.next?
s.new_workers = workers.next
s.current_workers = workers.next
else
# remember for later
s.current_workers = currentWorkers
return state
realizeState = (cfg, state, callback) ->
workers = []
for name, role of state
continue if role.error
continue if not role.new_workers?
workers.push
app: role.app
role: cfg[name].worker
quantity: role.new_workers
heroku.setWorkers cfg['*'], workers, (err) ->
return callback err, state if err
return callback null, state
# polyfill for Object.values...
objectValues = (obj) ->
vals = []
for key, val of obj
vals.push val
return vals
historyStateValid = (s) ->
return s.error or s.estimated_workers?
historyEntryValid = (r) ->
return objectValues(r).every historyStateValid
historyValid = (h) ->
checks =
isArray: Array.isArray h
entries: if h.length then h.every (e) -> historyEntryValid e else true
return objectValues(checks).every (p) -> p == true
# Throws Error if not valid
exports.validateHistory = validateHistory = (h) ->
throw new Error "Invalid history object: #{JSON.stringify(h)}" if not historyValid h
class Governor extends EventEmitter
constructor: (c) ->
= c
= []
= null
= Math.floor(['*'].history/['*'].pollinterval)
= ['*'].pollinterval*1000
start: () ->
runFunc = () =>
(err, state) =>
# error and state are emitted,
interval = setInterval runFunc,
runFunc() # do first iteration right now
stop: () ->
clearInterval if
updateHistory: (history) ->
validateHistory history
= history
= .slice Math.max(.length-, 0)
debug 'history length', .length
nextState: (queues, details, workers) ->
state = nextState , , queues, details, workers
history = .concat [ state ]
history
return state
runOnceInternal: (callback) ->
getQueues = (mainConfig) ->
bluebird.promisify(rabbitmq.getStats, multiArgs: true)(mainConfig)
.then (results) ->
r =
queues: results[0]
details: results[1]
return Promise.resolve r
getWorkers = bluebird.promisify heroku.getWorkers
bluebird.props(
rabbitmq: getQueues ['*']
workers: getWorkers
).then (current) =>
state = current.rabbitmq.queues, current.rabbitmq.details, current.workers
return bluebird.promisify(realizeState)(, state)
.asCallback callback
return null
runOnce: (callback) ->
(err, state) =>
debug 'ran iteration', err, state
'error', err if err
for name, role of state
'error', role.error if role.error
'state', state
return callback err, state
exports.Governor = Governor