actionhero
Version:
actionhero.js is a multi-transport API Server with integrated cluster capabilities and delayed tasks
209 lines (181 loc) • 7.83 kB
JavaScript
const uuid = require('uuid')
const async = require('async')
/**
* Redis helpers and connections.
*
* @namespace api.redis
* @property {Object} clients - Holds the redis clients. Contains 3 redis connections: 'client', 'subscriber' and 'tasks'. Configured via `api.config.redis`.
* @property {Object} clients.client - The main redis connection. Use this if you need direct access to redis.
* @property {Object} clients.subscriber - A Redis connection only listeneing for reids pub/sub events.
* @property {Object} clients.tasks - A Redis connection for use only in the task ssytem.
* @property {Object} subscriptionHandlers - Callbacks for redis pub/sub
* @property {Object} rpcCallbacks - RPC callbacks for responses to other clients
* @property {Object} status - Redis connection statuses
*/
module.exports = {
startPriority: 101,
stopPriority: 99999,
loadPriority: 200,
initialize: function (api, next) {
api.redis = {}
api.redis.clients = {}
api.redis.clusterCallbaks = {}
api.redis.clusterCallbakTimeouts = {}
api.redis.subscriptionHandlers = {}
api.redis.status = {
subscribed: false
}
api.redis.initialize = function (callback) {
let jobs = [];
['client', 'subscriber', 'tasks'].forEach((r) => {
jobs.push((done) => {
if (api.config.redis[r].buildNew === true) {
const args = api.config.redis[r].args
api.redis.clients[r] = new api.config.redis[r].konstructor(args[0], args[1], args[2]) // eslint-disable-line
api.redis.clients[r].on('error', (error) => { api.log(`Redis connection \`${r}\` error`, 'error', error) })
api.redis.clients[r].on('connect', () => { api.log(`Redis connection \`${r}\` connected`, 'debug') })
api.redis.clients[r].once('connect', done)
} else {
api.redis.clients[r] = api.config.redis[r].konstructor.apply(null, api.config.redis[r].args)
api.redis.clients[r].on('error', (error) => { api.log(`Redis connection \`${r}\` error`, 'error', error) })
api.log(`Redis connection \`${r}\` connected`, 'debug')
done()
}
})
})
if (!api.redis.status.subscribed) {
jobs.push((done) => {
api.redis.clients.subscriber.subscribe(api.config.general.channel)
api.redis.status.subscribed = true
api.redis.clients.subscriber.on('message', (messageChannel, message) => {
try { message = JSON.parse(message) } catch (e) { message = {} }
if (messageChannel === api.config.general.channel && message.serverToken === api.config.general.serverToken) {
if (api.redis.subscriptionHandlers[message.messageType]) {
api.redis.subscriptionHandlers[message.messageType](message)
}
}
})
done()
})
}
async.series(jobs, callback)
}
api.redis.publish = function (payload) {
const channel = api.config.general.channel
api.redis.clients.client.publish(channel, JSON.stringify(payload))
}
// Subsciption Handlers
api.redis.subscriptionHandlers['do'] = function (message) {
if (!message.connectionId || (api.connections && api.connections.connections[message.connectionId])) {
function callback () { // eslint-disable-line
let responseArgs = Array.apply(null, arguments).sort()
process.nextTick(() => {
api.redis.respondCluster(message.requestId, responseArgs)
})
};
let cmdParts = message.method.split('.')
let cmd = cmdParts.shift()
if (cmd !== 'api') { throw new Error('cannot operate on a method outside of the api object') }
let method = api.utils.dotProp.get(api, cmdParts.join('.'))
let args = message.args
if (args === null) { args = [] }
if (!Array.isArray(args)) { args = [args] }
args.push(callback)
if (method) {
method.apply(null, args)
} else {
api.log('RPC method `' + cmdParts.join('.') + '` not found', 'warning')
}
}
}
api.redis.subscriptionHandlers.doResponse = function (message) {
if (api.redis.clusterCallbaks[message.requestId]) {
clearTimeout(api.redis.clusterCallbakTimeouts[message.requestId])
api.redis.clusterCallbaks[message.requestId].apply(null, message.response)
delete api.redis.clusterCallbaks[message.requestId]
delete api.redis.clusterCallbakTimeouts[message.requestId]
}
}
/**
* Invoke a command on all servers in this cluster.
*
* @param {string} method The method to call on the remote server.
* @param {Array} args The arguments to pass to `method`
* @param {string} connectionId (optional) Should this method only apply to a server which `connectionId` is connected to?
* @param {Boolean} waitForResponse (optional) Should we await a response from a remote server in the cluster?
* @param {valueCallback} callback The return value from the remote server.
*/
api.redis.doCluster = function (method, args, connectionId, callback) {
const requestId = uuid.v4()
const payload = {
messageType: 'do',
serverId: api.id,
serverToken: api.config.general.serverToken,
requestId: requestId,
method: method,
connectionId: connectionId,
args: args // [1,2,3]
}
api.redis.publish(payload)
if (typeof callback === 'function') {
api.redis.clusterCallbaks[requestId] = callback
api.redis.clusterCallbakTimeouts[requestId] = setTimeout((requestId) => {
if (typeof api.redis.clusterCallbaks[requestId] === 'function') {
api.redis.clusterCallbaks[requestId](new Error('RPC Timeout'))
}
delete api.redis.clusterCallbaks[requestId]
delete api.redis.clusterCallbakTimeouts[requestId]
}, api.config.general.rpcTimeout, requestId)
}
}
/**
* This callback is invoked with an error or a response from the remote server.
* @callback valueCallback
* @param {Error} error An error or null.
* @param {object} value The response value from the remote server.
*/
api.redis.respondCluster = function (requestId, response) {
const payload = {
messageType: 'doResponse',
serverId: api.id,
serverToken: api.config.general.serverToken,
requestId: requestId,
response: response // args to pass back, including error
}
api.redis.publish(payload)
}
// Boot
api.redis.initialize(function (error) {
if (error) { return next(error) }
process.nextTick(next)
})
},
start: function (api, next) {
api.redis.doCluster('api.log', [`actionhero member ${api.id} has joined the cluster`], null, null)
next()
},
stop: function (api, next) {
for (let i in api.redis.clusterCallbakTimeouts) {
clearTimeout(api.redis.clusterCallbakTimeouts[i])
delete api.redis.clusterCallbakTimeouts[i]
delete api.redis.clusterCallbaks[i]
}
api.redis.doCluster('api.log', [`actionhero member ${api.id} has left the cluster`], null, null)
process.nextTick(function () {
api.redis.clients.subscriber.unsubscribe()
api.redis.status.subscribed = false;
['client', 'subscriber', 'tasks'].forEach((r) => {
let client = api.redis.clients[r]
if (typeof client.quit === 'function') {
client.quit()
} else if (typeof client.end === 'function') {
client.end()
} else if (typeof client.disconnect === 'function') {
client.disconnect()
}
})
next()
})
}
}