UNPKG

zmqbus

Version:

A broker-less message bus with failover

246 lines (218 loc) 7.97 kB
/*jshint laxcomma: true, smarttabs: true, node: true, esnext: true*/ 'use strict'; /** * Class to handler leader elections * @module zmqbus/lib/election * @author Edward Choh * @author Eric Satterwhite * @since 0.0.1 * @requires dgram * @requires events * @requires zmqbus/lib/common */ const dgram = require('dgram') , debug = require('debug')('zmqbus:node:elector') , {EventEmitter} = require('events') , {lpad, random, isNumber} = require('./common') ; const base_options = Object.freeze({ election_priority: 0 , election_timeout: 2000 , heartbeat_period: 2000 , heartbeat_timeout: 6000 , multicast_addr: '239.1.2.3' , multicast_port: 45555 }); /** * @alias module:zmqbus/lib/election * @param {Object} [options={}] * @param {Object} [metadata={}] * @fires module:zmqbus/lib/election/error * @fires module:zmqbus/lib/election/ready * @fires module:zmqbus/lib/election/elected */ class Elector extends EventEmitter{ constructor(options = {}, metadata = {}){ super(); this.options = Object.assign( {}, base_options, options ); // check for numbers for(let key in this.options){ if( /port/.test( key ) && !isNumber( this.options[key] ) ){ throw new Error(`${key} must be a number - ${this.options[key]}`); } } this.metadata = Object.assign( {}, metadata ); this.sock = dgram.createSocket({type:'udp4', reuseAddr: true}); this.id = lpad(99 - this.options.election_priority,2) + lpad( random(), 8) this.metadata.id = this.id; this.elect_last = {id: ''}; this.election_in_progress = false; this.elect_state = 0; this.last_hb_time = 0; /** * Error events emitted from the dgram socket * @name module:zmqbus/lib/election#error * @event * @param {TYPE} name description **/ this.sock.on('error', (err)=>{ this.emit( 'error', err ); }); this.sock.on('message', (msg, rinfo) =>{ try{ this.process_msg(JSON.parse(msg.toString()), rinfo); } catch(e){ // ignore malformed JSON return; } }); this.sock.bind(this.options.multicast_port, ()=>{ this.sock.setBroadcast(true); if( this.options.multicast_addr != '255.255.255.255' ){ this.sock.addMembership( this.options.multicast_addr ); } /** * Emitted when the dgram socket is ready * @name module:zmqbus/lib/election#ready * @event * @param {TYPE} name description **/ process.nextTick(()=>{ this.emit('ready', this, this.sock.address()); }); }); } /**  * Sends connection information over multicast * @protected  * @method module:zmqbus/lib/election#broadcast  * @param {Array} advert connection information object  **/ broadcast(advert){ if( !this.sock ){ return; } let msg = new Buffer(JSON.stringify(advert)); this.sock.send(msg, 0, msg.length, this.options.multicast_port, this.options.multicast_addr); } /**  * Responsible for parsing internal election messages * @private  * @method module:zmqbus/lib/election#process_msg  * @param {msg} the incoming message to parse  * @param {Object} netinfo Netork information relayed from the multicast router via libuv  **/ process_msg(msg, rinfo){ if( !Array.isArray(msg) || msg.length < 2){ return; } switch( msg[0] ){ case 'hb': if( this.elect_last.id == msg[1] ){ this.last_hb_time = new Date().getTime(); } return; case 'elect': return this.on_elect_msg( msg.slice(1) ); default: return; } /** * @name module:zmqbus/lib/election#event * @event * @param {TYPE} name description **/ this.emit('message', msg, rinfo); } /**  * Handler for elect events and deciding a winner of the election process * @private  * @method module:zmqbus/lib/election#on_elect_msg  * @param {Array} msg The incoming message  **/ on_elect_msg(msg){ if( msg[0] == 'start' ){ // broadcast self as candidate this.election_in_progress = true; this.broadcast( ['elect', 'candidate', this.metadata, this.elect_last] ); } else if( msg[0] == 'candidate' && this.elector ){ if( msg[1].id < this.elect_candidate.id ){ this.elect_candidate = msg[1]; } if( !this.elect_last.id ) { this.elect_last = msg[2]; } if( msg[1].id == this.elect_last.id ){ this.elect_last_seen = true; } } else if( msg[0] == 'winner' ){ debug(`node ${this.id} is the leader`) this.elect_last = msg[1]; this.election_in_progress = false; if(this.elect_last.id == this.id){ clearInterval(this.hb_handle); // this node got elected this.hb_handle = setInterval(()=>{ this.broadcast( ['hb', this.id] ); }, this.options.heartbeat_period); } /** * @name module:zmqbus/lib/election#event * @event * @param {TYPE} name description **/ this.emit('elected', this.elect_last); } else if( msg[0] == 'same' ){ this.election_in_progress = false; } } /**  * Starts the current node and participates in the peer ring and kicks off the election process  * @method module:zmqbus/lib/election#start  **/ start(){ this.checkHeartbeat(); this.checkHeartbeatHandle = setInterval(this.checkHeartbeat.bind( this ), this.options.heartbeat_period); } /**  * Determines if the current master has failed and starts a new election * @protected  * @method module:zmqbus/lib/election#checkHeartbeat  **/ checkHeartbeat(){ let delta = new Date().getTime() - this.last_hb_time; if (delta >= this.options.heartbeat_timeout){ // haven't heard from the elected, force an election debug('leader had died. starting new election', 'delta:', delta, 'last:', this.last_hb_time) this.start_election(); } } /**  * closes any open sockets and removes node from peer participation  * @method module:zmqbus/lib/election#stop  **/ stop(){ clearInterval(this.checkHeartbeatHandle); this.checkHeartbeatHandle = null; this.sock.close(); this.sock = null; } /**  * Internal method used to start the election process * @protected  * @method module:zmqbus/lib/election#start_election  **/ start_election(){ if( this.election_in_progress ){ return; } this.broadcast(['elect', 'start']); this.elector = true; this.elect_candidate = this.metadata; this.elect_last_seen = false; setTimeout(() =>{ if( this.elect_last.id != this.elect_candidate.id && !this.elect_last_seen ){ this.broadcast( ['elect', 'winner', this.elect_candidate] ); this.elect_last = this.elect_candidate; } else { this.broadcast( ['elect', 'winner', this.elect_last] ); } this.elector = false; }, this.options.election_timeout ); } } module.exports = Elector;