UNPKG

thywill

Version:

A Node.js clustered framework for single page web applications based on asynchronous messaging.

195 lines (177 loc) 6.08 kB
/** * @fileOverview * This script runs the RedisCluster heartbeat. * * It is launched by RedisCluster to run in a separate process. * * This is done to ensure that cluster heartbeats run on time no matter * what is going on in the main Thywill process. Thus heartbeats can be * modestly fast - bearing in mind that they run via Redis pub/sub and are * thus subject to any latency that might arise there. */ var cluster = require('cluster'); var redis = require('redis'); var Thywill = require('thywill'); var ThywillCluster = Thywill.getBaseClass('Cluster'); // -------------------------------------------------- // Functions // -------------------------------------------------- /** * Send a message back to the parent process. * * @param {mixed} data * The data to send as a message. */ function sendMessage (data) { process.send(JSON.stringify(data)); } /** * Send a message that will get written to the log in the parent process. * * @param {Error|string} message * The log message. * @param {string} level * The log level, one of those supported by Thywill. */ function sendLog (message, level) { if (message instanceof Error) { message = message.stack; } sendMessage({ type: 'log', level: level, message: message }); } // Various log level shortcuts. var log = { debug: function (message) { sendLog(message, 'debug'); }, info: function (message) { sendLog(message, 'info'); }, warn: function (message) { sendLog(message, 'warn'); }, error: function (message) { sendLog(message, 'error'); } }; /** * When a cluster member state changes, send an update. * * @return {string} clusterMemberId * ID of the cluster member that changed status. * @return {number} sinceLast * Milliseconds since last heartbeat. */ function sendDown (clusterMemberId, sinceLast) { sendMessage({ type: 'down', clusterMemberId: clusterMemberId, sinceLast: sinceLast }); } /** * When a cluster member state changes, send an update. * * @return {string} clusterMemberId * ID of the cluster member that changed status. * @return {string} status * The new status. */ function sendUp (clusterMemberId) { sendMessage({ type: 'up', clusterMemberId: clusterMemberId }); } // -------------------------------------------------- // Arguments // -------------------------------------------------- // Arguments arrive as a base64 encoded JSON string. // { // clusterMemberIds: array, // heartbeatInterval: number, // heartbeatTimeout: number, // localClusterMemberId: string, // redisPort: number, // redisHost: string, // redisOptions: object, // redisPrefix: string // }; var args = new Buffer(process.argv[2], 'base64').toString('utf8'); args = JSON.parse(args); // -------------------------------------------------- // Redis Connections // -------------------------------------------------- var subscribeRedisClient = redis.createClient(args.redisPort, args.redisHost, args.redisOptions); var publishRedisClient = redis.createClient(args.redisPort, args.redisHost, args.redisOptions); // Dummy instance to use for protecting these clients. This is somewhat hacky and fragile. var thywill = new Thywill(); thywill.log = log; thywill.protectRedisClient(subscribeRedisClient); thywill.protectRedisClient(publishRedisClient); // -------------------------------------------------- // Heartbeat // -------------------------------------------------- var heartbeatChannel = args.redisPrefix + 'heartbeat'; var heartbeatTimestamps = {}; var heartbeatStatus = {}; // Cluster member status starts out UNKNOWN - no alerting happens unless // status switches from UP to DOWN or vice versa. A switch from UNKNOWN // to one of those two does nothing. args.clusterMemberIds.forEach(function (clusterMemberId, index, array) { heartbeatStatus[clusterMemberId] = ThywillCluster.CLUSTER_MEMBER_STATUS.UNKNOWN; }); // Set a process to detect failed heartbeats by timeout. var heartbeatCheckIntervalId = setInterval(function () { // We only check timestamps that have previously arrived - properties of // the timestamps object aren't set until the first heartbeat from a given // cluster member. This makes things less confused during startup of // multiple processes. for (var clusterMemberId in heartbeatTimestamps) { // Only bother checking those that are marked up. if (heartbeatStatus[clusterMemberId] !== ThywillCluster.CLUSTER_MEMBER_STATUS.UP) { return; } var sinceLast = Date.now() - heartbeatTimestamps[clusterMemberId]; if (sinceLast > args.heartbeatTimeout) { heartbeatStatus[clusterMemberId] = ThywillCluster.CLUSTER_MEMBER_STATUS.DOWN; sendDown(clusterMemberId, sinceLast); } } }, args.heartbeatInterval); // Emit heartbeats at an interval. var lastHeartbeat; var lagDelay = 1.5 * args.heartbeatInterval; var heartbeatIntervalId = setInterval(function () { var timestamp = Date.now(); if (lastHeartbeat) { var since = timestamp - lastHeartbeat; if (since > lagDelay) { log.debug( 'RedisClusterHeartbeat: heartbeat interval is lagging: ' + since + 'ms since last and should be ' + args.heartbeatInterval + 'ms.' ); } } lastHeartbeat = timestamp; publishRedisClient.publish(heartbeatChannel, args.localClusterMemberId); }, args.heartbeatInterval); // Listen for heartbeats from other cluster members and update the local // timestamps and status accordingly. subscribeRedisClient.subscribe(heartbeatChannel); subscribeRedisClient.on('message', function (channel, clusterMemberId) { // Ignore the heartbeat for this process. if (clusterMemberId === args.localClusterMemberId) { return; } heartbeatTimestamps[clusterMemberId] = Date.now(); // If that cluster member was in down or unknown status, then note the status change. if (heartbeatStatus[clusterMemberId] !== ThywillCluster.CLUSTER_MEMBER_STATUS.UP) { heartbeatStatus[clusterMemberId] = ThywillCluster.CLUSTER_MEMBER_STATUS.UP; sendUp(clusterMemberId); } });