guvnor
Version:
A node process manager that isn't spanners all the way down
391 lines (308 loc) • 9.84 kB
JavaScript
var Autowire = require('wantsit').Autowire
var semver = require('semver')
var async = require('async')
var HostData = function (name, data) {
this._logger = Autowire
this._config = Autowire
this._processDataFactory = Autowire
this._webSocketResponder = Autowire
this._remote = Autowire
this.name = name
this.host = data.host
this.lastUpdated = Date.now()
Object.defineProperties(this, {
'processes': {
value: [],
writable: true
},
'apps': {
value: [],
writable: true
},
'port': {
value: data.port
},
'_data': {
value: data
},
'_daemon': {
value: null,
writable: true
},
'_updateServerStatusTimeout': {
value: null,
writable: true
},
'_updateProcessesTimeout': {
value: null,
writable: true
},
'_timeoutThreshold': {
value: 60000
}
})
}
HostData.prototype.afterPropertiesSet = function () {
this._connectToDaemon()
}
HostData.prototype._connectToDaemon = function () {
this.status = 'connecting'
this._remote(this._logger, this._data, this._connectedToDaemon.bind(this))
}
HostData.prototype._connectedToDaemon = function (error, guvnor) {
if (error) {
if (error.code === 'CONNECTIONREFUSED') {
this.status = 'connectionrefused'
} else if (error.code === 'CONNECTIONRESET') {
this.status = 'connectionreset'
} else if (error.code === 'HOSTNOTFOUND') {
this.status = 'hostnotfound'
} else if (error.code === 'TIMEDOUT') {
this.status = 'connectiontimedout'
} else if (error.code === 'NETWORKDOWN') {
this.status = 'networkdown'
} else {
this._logger.error('Error connecting to guvnor', this._data.host, this._data.port, this._data.user)
this._logger.error('Message', error.message)
this._logger.error('Code', error.code)
this._logger.error('Stack', error.stack)
this.status = 'error'
}
return
}
if (this._daemon) {
// remove previous listener
this._daemon.off('disconnected')
}
this._daemon = guvnor
for (var key in this._daemon) {
if (key.charAt(0) !== '_' && typeof this._daemon[key] === 'function') {
this[key] = this._daemon[key].bind(this._daemon)
}
}
// listen for disconnection
this._daemon.once('disconnected', function () {
this._daemon.off('*')
this._daemon.off('process:uncaughtexception')
this._daemon.off('process:heapdump:complete')
this._daemon.off('process:heapdump:removed')
this._daemon.off('process:failed')
this._daemon.off('process:starting')
this._daemon.off('process:exit')
this._daemon.off('process:log:*')
this._daemon.off('cluster:log:*')
this._daemon.off('worker:log:*')
clearTimeout(this._updateServerStatusTimeout)
clearTimeout(this._updateProcessesTimeout)
this.status = 'connecting'
}.bind(this))
this._daemon.getDetails(function (error, details) {
if (error) {
this._logger.error('Error getting guvnor details', error)
this._handleRemoteError(error)
return
}
for (var key in details) {
this[key] = details[key]
}
if (!semver.satisfies(details.guvnor || details.boss, this._config.minVersion)) {
this.status = 'incompatible'
// tell everyone we are incompatible
var broadcast = function () {
this._webSocketResponder.broadcast('server:status', this.name, this)
setTimeout(broadcast, this._config.frequency).unref()
}.bind(this)
broadcast()
return
}
this.status = 'connected'
// update details
this._update('getServerStatus', this._handleUpdatedServerStatus, '_updateServerStatusTimeout')
this._update('listProcesses', this._handleUpdatedProcesses, '_updateProcessesTimeout')
this._listenForLogEvents()
this._listenForUncaughtExceptions()
this._listenForHeapSnapshots()
this._daemon.on('process:starting', this._createOrUpdateProcess.bind(this))
this._daemon.on('*', function () {
var args = Array.prototype.slice.call(arguments)
args.splice(1, 0, this.name)
this._webSocketResponder.broadcast.apply(this._webSocketResponder, args)
}.bind(this))
}.bind(this))
}
HostData.prototype._listenForLogEvents = function () {
this._daemon.on('process:log:*', function (type, process, event) {
var proc = this.findProcessById(process.id)
if (!proc) {
return
}
proc.log(type.split(':')[2], event.date, event.message)
}.bind(this))
this._daemon.on('cluster:log:*', function (type, cluster, event) {
var proc = this.findProcessById(cluster.id)
if (!proc) {
return
}
proc.log(type.split(':')[2], event.date, event.message)
}.bind(this))
this._daemon.on('worker:log:*', function (type, cluster, worker, event) {
var proc = this.findProcessById(worker.id)
if (!proc) {
return
}
proc.log(type.split(':')[2], event.date, event.message)
}.bind(this))
}
HostData.prototype._listenForUncaughtExceptions = function () {
this._daemon.on('process:uncaughtexception', function (process, event) {
var proc = this.findProcessById(process.id)
if (!proc) {
return
}
proc.exception(event.date, event.message, event.code, event.stack)
}.bind(this))
this._daemon.on('process:failed', function (process, error) {
var proc = this.findProcessById(process.id)
if (!proc) {
return
}
proc.exception(error.date, error.message, error.code, error.stack)
}.bind(this))
}
HostData.prototype._listenForHeapSnapshots = function () {
this._daemon.on('process:heapdump:complete', function (process, snapshot) {
var proc = this.findProcessById(process.id)
if (!proc) {
return
}
proc.snapshot(snapshot.id, snapshot.date, snapshot.path, snapshot.size)
}.bind(this))
this._daemon.on('process:heapdump:removed', function (process, snapshot) {
var proc = this.findProcessById(process.id)
if (!proc) {
return
}
proc.removeSnapshot(snapshot.id)
}.bind(this))
this._daemon.on('process:exit', function (process, snapshot) {
var proc = this.findProcessById(process.id)
if (!proc) {
return
}
proc.snapshots = []
}.bind(this))
}
HostData.prototype._handleRemoteError = function (error) {
if (error.code === 'TIMEOUT') {
if ((Date.now() - this.lastUpdated) < this._timeoutThreshold) {
// saw a different update in the last minute, ignore this timeout
this._logger.info('Ignoring update processes timeout for %s as we saw an update %ss ago', this.name, (Date.now() - this.lastUpdated) / 1000)
} else {
this.status = 'timeout'
}
} else if (error.code === 'INVALIDSIGNATURE') {
this.status = 'badsignature'
} else {
this.status = 'error'
}
}
HostData.prototype._update = function (method, update, timeoutName) {
var retry = function () {
if (this[timeoutName]) {
clearTimeout(this[timeoutName])
}
this[timeoutName] = setTimeout(this._update.bind(this, method, update, timeoutName), this._config.frequency)
this[timeoutName].unref()
}.bind(this)
this._daemon[method](function (error) {
if (error) {
this._handleRemoteError(error)
retry()
this._logger.error('Error getting guvnor status from', this.name, error)
} else {
if (this.status === 'timeout') {
this._logger.info(this.name, 'came back!')
}
this.status = 'connected'
this.lastUpdated = Date.now()
var args = Array.prototype.slice.call(arguments, 1)
args.push(retry)
update.apply(this, args)
}
}.bind(this))
}
HostData.prototype._handleUpdatedServerStatus = function (status, callback) {
for (var key in status) {
this[key] = status[key]
}
this._webSocketResponder.broadcast('server:status', this.name, this)
callback()
}
HostData.prototype._handleUpdatedProcesses = function (processes, callback) {
this._removeMissingProcesses(processes)
var tasks = processes.map(function (data) {
return this._createOrUpdateProcess.bind(this, data)
}.bind(this))
async.parallel(tasks, function (error) {
this._webSocketResponder.broadcast('server:processes', this.name, this.processes)
callback(error)
}.bind(this))
}
HostData.prototype._createOrUpdateProcess = function (data, callback) {
var existingProcess = this.findProcessById(data.id)
if (!existingProcess) {
this._processDataFactory.create([data], function (error, newProcess) {
if (error) {
return callback(error)
}
this.processes.push(newProcess)
if (callback) {
callback()
}
}.bind(this))
} else {
existingProcess.update(data)
if (callback) {
callback()
}
}
}
HostData.prototype._removeMissingProcesses = function (reportedProcesses) {
this.processes = this.processes.filter(function (existingProcess) {
for (var i = 0; i < reportedProcesses.length; i++) {
if (reportedProcesses[i].id === existingProcess.id) {
return true
}
}
return false
})
}
HostData.prototype.findApps = function (callback) {
if (!this._daemon) {
return callback(undefined, [])
}
this._daemon.listApplications(callback)
}
HostData.prototype.findUsers = function (callback) {
if (!this._daemon) {
return callback(undefined, [])
}
this._daemon.listUsers(callback)
}
HostData.prototype.findProcessById = function (id) {
for (var i = 0; i < this.processes.length; i++) {
var proc = this.processes[i]
if (proc.id === id) {
return proc
}
if (proc.workers) {
for (var n = 0; n < proc.workers.length; n++) {
if (proc.workers[n].id === id) {
return proc.workers[n]
}
}
}
}
return null
}
module.exports = HostData