es6/Services/Advertiser.js
import os from 'os'
import Promise from 'bluebird'
import genName from 'project-name-generator'
import thinky from '../thinky'
import Loop from './Loop'
import Registry from './Registry'
import Table from '../utils/Table'
import Events from '../utils/Events'
import fs from '../utils/fs'
const {type, r} = thinky
/**
* API that handles the lifecycle for updating and maintaing local copies of a Discovery
*/
export default class Discovery extends Table {
/**
* normalizes the `os.interfaces` Object
*
* @return {Array} os.interfaces() => Array
*/
static get interfaces () {
const ifaces = os.networkInterfaces()
const allAddresses = {}
Object.keys(ifaces).forEach(function (iface) {
const addresses = {}
let hasAddresses = false
ifaces[iface].forEach(function (address) {
if (!address.internal) {
addresses[(address.family || "").toLowerCase()] = address.address
hasAddresses = true
if (address.mac) {
addresses.mac = address.mac
}
}
})
if (hasAddresses) {
allAddresses[iface] = addresses
}
})
return Object.keys(allAddresses).reduce( (addresses, address) => {
allAddresses[address].name = address
return addresses.concat([ allAddresses[address] ])
}, [])
}
/**
* initializes and resolves to an Advertiser instance
*
* @param {Object} settings The settings
*/
constructor (settings = {}) {
super()
return this.table.ready()
.bind(this)
.then(this.loadContext)
.then(this.advertise)
.return(this)
}
/**
* returns the location where an Instance's id file should reside
*
* @property file
* @return {String}
*/
get file () {
return Registry.appDir('id.json')
}
/**
* returns a snapshot of the current Instance
*
* @attribute attributes
* @return {Object} the snapshot to use in a heartbeat
*
*/
get attributes () {
return {
id : this.id
, heartbeat : Date.now()
, load : os.loadavg()
, interfaces : Discovery.interfaces
, cpus : os.cpus()
}
}
/**
* generates the Instance context
*
* @return {Promise}
*/
loadContext () {
return Promise.props({
loop : Loop.create()
, id : this.loadId()
}).then( props => Object.assign(this, props) )
}
/**
* Determines if it has id file.
*
* @return {Promise} resolves to True if has id file, False otherwise.
*/
hasIdFile () {
return fs.stat$( this.file ).catch( err => {
if ( err.code === "ENOENT" ) return false
throw err
})
}
/**
* fetches or creates a new ID for the Instance
*
* @return {Promise} Resolve to the id
*/
loadId () {
return Registry.ensureAppDir()
.bind(this)
.then(this.hasIdFile)
.then( exists => {
return exists
? true
: r.uuid().run().bind(this).then(this.saveId)
})
.then( _=> require(this.file).id )
}
/**
* saves the id to the local filesystem to survive Instance restarts
*
* @param {String} id the id to be persisted
* @return {Promise}
*/
saveId (id) {
return fs.writeJson$( this.file, { id: id } ).then( _ => {
return Discovery.row(this.attributes).save()
})
}
/**
* heartbeat to let other Services know that this one is alive
*
* @return {Promise}
*/
heartbeat () {
return this.table.insert(this.attributes, { conflict : "update" }).run().then( instance => {
return this.emit(Discovery.Event.hb, instance)
})
}
/**
* main loop that generates heartbeats
*
* @return {null}
*/
advertise () {
this.advertising = true
this.loop.on( Loop.Event.tick, done => {
return this.heartbeat()
.then(done)
})
this.loop.start()
}
/**
* kills the Advertiser instance and makes sure we don't spawn a ZOMBIEEEEE
*/
kill () {
this.removeAllListeners()
this.loop.kill()
}
}
Discovery.Event = {
join : "discovery:join"
, sibling : "discovery:sibling"
, error : "discovery:error"
, hb : "discovery:heartbeat"
}
Events.methodize( Discovery, Discovery.Event )
Discovery.createModel( 'Services', {
name : type.string().default( function () { return genName({ words: 4, number: true }).dashed })
, service : type.string().default("wyst:queue")
, host : type.string().default(os.hostname)
, os : type.string().default(os.type)
, cpus : type.array().default(os.cpus)
, load : type.array().default(os.loadavg)
, interfaces : type.array().default( function () { return Discovery.interfaces })
, created_at : type.date().default(Date.now)
, heartbeat : type.date().default(Date.now)
})