/* options includes: - host - port - masterOptions - masterName - logger (e.g. winston) - debug (boolean) */ function RedisSentinelClient(options) { // RedisClient takes (stream,options). we don't want stream, make sure only one. if (arguments.length > 1) { throw new Error("Sentinel client takes only options to initialize"); } // make this an EventEmitter (also below) events.EventEmitter.call(this); var self = this; this.options = options = options || {}; this.options.masterName = this.options.masterName || 'mymaster'; self.emitMasterErrors = false; // no socket support for now (b/c need multiple connections). if (options.port == null || options.host == null) { throw new Error("Sentinel client needs a host and port"); } // if debugging is enabled for sentinel client, enable master client's too. // (standard client just uses console.log, not the 'logger' passed here.) if (options.debug) { RedisSingleClient.debug_mode = true; } var masterOptions = options.masterOptions || {}; masterOptions.disable_flush = true; // Disables flush_and_error, to preserve queue // if master & slaves need a password to authenticate, // pass it in as 'master_auth_pass'. // (corresponds w/ 'auth_pass' for normal client, // but differentiating b/c we're not authenticating to the *sentinel*, rather to the master/slaves.) // by setting it to 'auth_pass' on master client, it should authenticate to the master (& slaves on failover). // note, sentinel daemon's conf needs to know this same password too/separately. masterOptions.auth_pass = options.master_auth_pass || null; // this client will always be connected to the active master. // using 9999 as initial; expected to fail; is replaced & re-connected to real port later. self.activeMasterClient = new RedisSingleClient.createClient(9999, '127.0.0.1', masterOptions); // pass up errors // @todo emit a separate 'master error' event? self.activeMasterClient.on('error', function(error){ if (self.emitMasterErrors) { error.message = self.myName + " master error: " + error.message; self.onError.call(self, error); } }); // pass up messages self.activeMasterClient.on('message', function(channel, message){ self.emit('message', channel, message); }); // pass up pmessage self.activeMasterClient.on('pmessage', function(pattern, channel, message){ self.emit('pmessage', pattern, channel, message); }); // pass these through ['unsubscribe','end', 'reconnecting'].forEach(function(staticProp){ // @todo rewrite this to use `apply` self.activeMasterClient.on(staticProp, function(a, b, c, d){ self.emit(staticProp, a, b, c, d); }); }); // used for logging & errors this.myName = 'sentinel-' + this.options.host + ':' + this.options.port + '-' + this.options.masterName; /* what a failover looks like: - master fires ECONNREFUSED errors a few times - sentinel listener gets: +sdown +odown +failover-triggered +failover-state-wait-start +failover-state-select-slave +selected-slave +failover-state-send-slaveof-noone +failover-state-wait-promotion +promoted-slave +failover-state-reconf-slaves +slave-reconf-sent +slave-reconf-inprog +slave-reconf-done +failover-end +switch-master (see docs @ http://redis.io/topics/sentinel) note, these messages don't specify WHICH master is down. so if a sentinel is listening to multiple masters, and we have a RedisSentinelClient for each sentinel:master relationship, every client will be notified of every master's failovers. But that's fine, b/c reconnect() checks if it actually changed, and does nothing if not. */ // one client to query ('talker'), one client to subscribe ('listener'). // these are standard redis clients. // talker is used by reconnect() below this.sentinelTalker = new RedisSingleClient.createClient(options.port, options.host); this.sentinelTalker.on('connect', function(){ self.debug('connected to sentinel talker'); }); this.sentinelTalker.on('error', function(error){ error.message = self.myName + " talker error: " + error.message; self.onError.call(self, error); }); this.sentinelTalker.on('end', function(){ self.debug('sentinel talker disconnected'); // @todo emit something? // @todo does it automatically reconnect? (supposed to) }); var sentinelListener = new RedisSingleClient.createClient(options.port, options.host); sentinelListener.on('connect', function(){ self.debug('connected to sentinel listener'); }); sentinelListener.on('error', function(error){ error.message = self.myName + " listener error: " + error.message; self.onError(error); }); sentinelListener.on('end', function(){ self.debug('sentinel listener disconnected'); // @todo emit something? }); // Connect on load this.reconnect(); // Subscribe to all messages sentinelListener.psubscribe('*'); sentinelListener.on('pmessage', function(channel, msg) { self.debug('sentinel message', msg); // pass up, in case app wants to respond self.emit('sentinel message', msg); switch(msg) { case '+sdown': self.debug('Down detected'); self.emit('down-start'); self.emitMasterErrors = false; break; case '+failover-triggered': self.debug('Failover detected'); self.emit('failover-start'); break; case '+switch-master': self.debug('Reconnect triggered by ' + msg); self.emit('failover-end'); self.reconnect(); break; } }); }