├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── index.js ├── lib ├── ServerAdapter.js ├── SocketAdapter.js ├── accrual_failure_detector.js ├── gossiper.js ├── peer_state.js └── scuttle.js ├── package.json ├── simulation ├── example.js ├── s1.js ├── s2.js └── s3.js └── test ├── NetworkAdapters.test.js ├── accrual_failure_detector.test.js~ ├── gossiper.real.test.js ├── gossiper.test.js ├── peer_state.test.js~ ├── scuttle.test.js~ ├── secure.real.test.js └── securesocket.js~ /.gitignore: -------------------------------------------------------------------------------- 1 | *.*rc 2 | node_modules 3 | npm_debug.log -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.10' 4 | - '0.12' -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2010 Bob Potter. All rights reserved. 2 | Permission is hereby granted, free of charge, to any person obtaining a copy 3 | of this software and associated documentation files (the "Software"), to 4 | deal in the Software without restriction, including without limitation the 5 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 6 | sell copies of the Software, and to permit persons to whom the Software is 7 | furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in 10 | all copies or substantial portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 13 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 14 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 15 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 16 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 17 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 18 | IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Grapevine [![Build Status](https://secure.travis-ci.org/kessler/grapevine.png?branch=master)](http://travis-ci.org/kessler/grapevine) 2 | ====== 3 | 4 | _A fork of the original node-gossip_ 5 | 6 | > grape·vine (grāp′vīn′) n. 7 | > 1. A vine on which grapes grow. 8 | > 2. 9 | > a. The informal transmission of information, gossip, or rumor from person to person. 10 | > b. A usually unrevealed source of confidential information. 11 | 12 | **Please note**: Version 1.0.0+ has breaking changes and cannot transparently replace 0.\*.\* versions 13 | 14 | #### New features: 15 | * default transport using [nssocket](https://github.com/nodejitsu/nssocketa) 16 | * key/value pairs have optional expiry, which propagates to the other peers, it will cause keys to get deleted (although this is not an EXACT mechanism, so it shouldn't be used as such) 17 | * IPv6 support 18 | * various bug fixes 19 | 20 | node-gossip implements a gossip protocol w/failure detection, allowing you to create a fault-tolerant, self-managing cluster of node.js processes. Each server in the cluster has it's own set of key-value pairs which are propogated to the others peers in the cluster. The API allows you to make changes to the local state, listen for changes in state, listen for new peers and be notified when a peer appears to be dead or appears to have come back to life. 21 | 22 | Check out the the scripts in the simulations/ directory for some examples. 23 | 24 | ### Usage 25 | ```javascript 26 | var Gossiper = require('grapevine'); 27 | // Create a seed peer. 28 | var seed = new Gossiper({ port: 9000 }); 29 | seed.start(); 30 | 31 | // Create 20 new peers and point them at the seed (usually this would happen in 20 separate processes) 32 | // To prevent having a single point of failure you would probably have multiple seeds 33 | for(var i = 9001; i <= 9020;i++) { 34 | //For IPv6 peers use the format [ad:dre::ss]:port. e.g. [::1]:9000 35 | var g = new Gossiper({port: i, seeds:['127.0.0.1:9000'] }); 36 | g.start(); 37 | 38 | g.on('update', function(peer, k, v) { 39 | console.log("peer " + peer + " set " + k + " to " + v); // peer 127.0.0.1:9999 set somekey to somevalue 40 | }); 41 | } 42 | 43 | // Add another peer which updates it's state after 15 seconds 44 | var updater = new Gossiper({ port: 9999, seeds: ['127.0.0.1:9000'] }); 45 | updater.start(); 46 | setTimeout(function() { 47 | updater.setLocalState('somekey', 'somevalue'); 48 | // with expiry 49 | updater.setLocalState('somekey', 'somevalue', Date.now() + 10000); // 10 seconds from now this key will start to expire in the gossip net 50 | }, 15000); 51 | ``` 52 | 53 | ### API 54 | 55 | Gossiper methods: 56 | ```javascript 57 | allPeers() 58 | livePeers() 59 | deadPeers() 60 | peerValue(peer, key) 61 | peerKeys(peer) 62 | getLocalState(key) 63 | setLocalSate(key, value) 64 | ``` 65 | Gossiper events: 66 | ```javascript 67 | on('update', function(peer_name, key, value) {}) 68 | on('new_peer', function(peer_name) {}) 69 | on('peer_alive', function(peer_name) {}) 70 | on('peer_failed', function(peer_name) {}) 71 | ``` 72 | ### Tests 73 | 74 | expresso -I lib test/* 75 | 76 | ### TODO 77 | 78 | * major code refactoring, too many people wrote too much code without proper coordination 79 | * convert tests to mocha - partially completed 80 | * test edge cases 81 | * Cluster name -- dont allow peers to accidentally join the wrong cluster 82 | * The scuttlebutt paper mentions a couple things we don't current do: 83 | * congestion throttling 84 | * make digests only be random subsets 85 | 86 | ### Acknowledgements 87 | 88 | Both the gossip protocol and the failure detection algorithms are based off of academic papers and Cassandra's (http://www.cassandra.org/) implementation of those papers. This library is highly indebted to both. 89 | 90 | * ["Efficient reconciliation and flow control for anti-entropy protocols"](http://www.cs.cornell.edu/home/rvr/papers/flowgossip.pdf) 91 | * ["The Phi accrual failure detector"](http://vsedach.googlepages.com/HDY04.pdf) 92 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports.Gossiper = require('./lib/gossiper') 2 | module.exports.ServerAdapter = require('./lib/ServerAdapter') 3 | module.exports.SocketAdapter = require('./lib/SocketAdapter') 4 | 5 | module.exports.simpleSecureGossiper = function (options, callback) { 6 | var pem = require('pem') 7 | 8 | pem.createCertificate({ 9 | days: 1, 10 | selfSigned: true 11 | }, function(err, keys) { 12 | if (err) return callback(err) 13 | 14 | options.secure = true 15 | options.type = 'tls' 16 | options.key = keys.serviceKey 17 | options.cert = keys.certificate 18 | options.rejectUnauthorized = false 19 | options.secureProtocol = 'TLSv1_method' 20 | 21 | var gossiper = new module.exports.Gossiper(options) 22 | 23 | callback(null, gossiper) 24 | }) 25 | } -------------------------------------------------------------------------------- /lib/ServerAdapter.js: -------------------------------------------------------------------------------- 1 | var debug = require('debug')('grapevine_ServerAdapter'); 2 | var nssocket = require('nssocket') 3 | var inherits = require('util').inherits 4 | var inspect = require('util').inspect 5 | var EventEmitter = require('events').EventEmitter 6 | var SocketAdapter = require('./SocketAdapter') 7 | 8 | module.exports = ServerAdapter 9 | 10 | /* 11 | * Adapts nssocket server for a gossiper. 12 | * 13 | * This adapter works in par with the default SocketAdapter. 14 | * 15 | * A gossiper expects the following interface and events from a ServerAdapter: 16 | * - a "connection" event when a socket connects to the server 17 | * - a listen method, which will emit a "listening" event when the server is successfully 18 | * listening to the desired port 19 | * - a close method, which will emit a "close" event when the server is shut down 20 | * - an "error" event 21 | * @class 22 | */ 23 | inherits(ServerAdapter, EventEmitter) 24 | function ServerAdapter(options) { 25 | EventEmitter.call(this) 26 | 27 | options = options || {} 28 | 29 | var self = this 30 | 31 | this._server = nssocket.createServer(options, function(socket) { 32 | debug('incoming connection') 33 | var adapter = new SocketAdapter(undefined, socket) 34 | self.emit('connection', adapter) 35 | }) 36 | 37 | this._server.on('error', function(e) { 38 | if (debug.enabled) { 39 | debug('server error %s', inspect(e)) 40 | } 41 | 42 | self.emit('error', e) 43 | }) 44 | 45 | this._server.on('clientError', function (e) { 46 | if (debug.enabled) { 47 | debug('client error %s', inspect(e)) 48 | } 49 | 50 | self.emit('error', e) 51 | }) 52 | 53 | this._server.on('listening', function () { 54 | debug('server listening %s:%s', self.address, self.port) 55 | self.emit('listening') 56 | }) 57 | 58 | this._server.on('close', function () { 59 | debug('server closed %s:%s', self.address, self.port) 60 | self.emit('close') 61 | }) 62 | } 63 | 64 | ServerAdapter.prototype.listen = function(port, address) { 65 | if (!port) { 66 | throw new Error('must provide a port') 67 | } 68 | 69 | this.port = port 70 | this.address = address || '127.0.0.1' 71 | 72 | this._server.listen(port, address) 73 | } 74 | 75 | ServerAdapter.prototype.close = function() { 76 | debug('server closing %s:%s', this.address, this.port) 77 | this._server.close() 78 | } 79 | -------------------------------------------------------------------------------- /lib/SocketAdapter.js: -------------------------------------------------------------------------------- 1 | var debug = require('debug')('grapevine_SocketAdapter'); 2 | var nssocket = require('nssocket') 3 | var inherits = require('util').inherits 4 | var inspect = require('util').inspect 5 | var EventEmitter = require('events').EventEmitter 6 | 7 | module.exports = SocketAdapter 8 | 9 | /* 10 | * TODO: complete documentation 11 | * 12 | * @class 13 | */ 14 | inherits(SocketAdapter, EventEmitter) 15 | function SocketAdapter(options, nsSocket) { 16 | EventEmitter.call(this) 17 | 18 | this.options = options 19 | this._nsSocket = nsSocket 20 | 21 | if (this._nsSocket) { 22 | this._hookEvents() 23 | } 24 | } 25 | 26 | SocketAdapter.prototype.connect = function(port, address) { 27 | if (this._nsSocket) { 28 | throw new Error('already connected') 29 | } 30 | 31 | if (!port) { 32 | throw new Error('must provide a port') 33 | } 34 | 35 | address = address || '127.0.0.1' 36 | 37 | this._nsSocket = new nssocket.NsSocket(this.options) 38 | this._hookEvents() 39 | debug('socket connecting to %s:%s', address, port) 40 | this._nsSocket.connect(port, address) 41 | } 42 | 43 | SocketAdapter.prototype._hookEvents = function () { 44 | var self = this 45 | 46 | this._nsSocket.data(['msg'], function (message) { 47 | self.emit('data', message, self) 48 | }) 49 | 50 | this._nsSocket.on('start', function () { 51 | self.emit('connect') 52 | }) 53 | 54 | this._nsSocket.on('close', function () { 55 | self.emit('close') 56 | }) 57 | 58 | this._nsSocket.on('destroy', function () { 59 | self.emit('destroy') 60 | }) 61 | 62 | this._nsSocket.on('error', function (e) { 63 | self.emit('error', e) 64 | }) 65 | } 66 | 67 | SocketAdapter.prototype.write = function (message) { 68 | if (debug.enabled) { 69 | debug('sending message %s', inspect(message)) 70 | } 71 | 72 | this._nsSocket.send(['msg'], message) 73 | } 74 | 75 | SocketAdapter.prototype.end = function () { 76 | if (this._nsSocket) { 77 | debug('socket closing') 78 | this._nsSocket.end() 79 | } 80 | } 81 | 82 | SocketAdapter.prototype.destroy = function () { 83 | if (this._nsSocket) { 84 | debug('socket destroy') 85 | this._nsSocket.destroy() 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /lib/accrual_failure_detector.js: -------------------------------------------------------------------------------- 1 | module.exports = AccrualFailureDetector; 2 | 3 | function AccrualFailureDetector() { 4 | this.last_time = undefined; 5 | this.intervals = []; 6 | } 7 | 8 | AccrualFailureDetector.prototype.add = function(arrival_time) { 9 | if(this.last_time == undefined) { 10 | var i = 750; 11 | } else { 12 | var i = arrival_time - this.last_time; 13 | } 14 | 15 | this.last_time = arrival_time; 16 | this.intervals.push(i); 17 | if(this.intervals.length > 1000) { 18 | this.intervals.shift(); 19 | } 20 | }; 21 | 22 | AccrualFailureDetector.prototype.phi = function(current_time) { 23 | var current_interval = current_time - this.last_time; 24 | var exp = -1 * current_interval / this.interval_mean(); 25 | 26 | var p = Math.pow(Math.E, exp); 27 | return -1 * (Math.log(p) / Math.log(10)); 28 | }; 29 | 30 | AccrualFailureDetector.prototype.interval_mean = function(current_time) { 31 | var sum = 0; 32 | for(var i in this.intervals) { 33 | sum += this.intervals[i]; 34 | } 35 | return sum / this.intervals.length; 36 | }; 37 | -------------------------------------------------------------------------------- /lib/gossiper.js: -------------------------------------------------------------------------------- 1 | var PeerState = require('./peer_state'); 2 | var Scuttle = require('./scuttle'); 3 | var EventEmitter = require('events').EventEmitter; 4 | var net = require('net'); 5 | var util = require('util'); 6 | var child_process = require('child_process'); 7 | var dns = require('dns'); 8 | var debug = require('debug')('grapevine_Gossiper'); 9 | var nssocket = require('nssocket'); 10 | var ServerAdapter = require('./ServerAdapter') 11 | var SocketAdapter = require('./SocketAdapter') 12 | 13 | module.exports = Gossiper 14 | 15 | /* 16 | * TODO: complete documentation 17 | * 18 | * the flow of data is as follows: 19 | * 1. every peer is a server 20 | * 2. a peer randomly connects to another peer with a message of type REQUEST 21 | * 3. the other peer responds with a message of type FIRST_RESPONSE 22 | * 4. the initiating peer responds with a message of type SECOND_RESPONSE 23 | * 24 | * @class 25 | */ 26 | util.inherits(Gossiper, EventEmitter); 27 | function Gossiper(options) { 28 | EventEmitter.call(this); 29 | 30 | if (typeof options === 'number') { 31 | options = { 32 | port: options 33 | }; 34 | } 35 | 36 | options = options || {}; 37 | 38 | if (typeof options.port !== 'number') 39 | throw new Error('must specify a port'); 40 | 41 | // TODO eek refactor: 42 | this._newServerAdapter = options.newServerAdapter || function () { return new ServerAdapter(options); }; 43 | this._newSocketAdapter = options.newSocketAdapter || function () { return new SocketAdapter(options); }; 44 | 45 | this.address = options.address || '127.0.0.1'; 46 | this.port = options.port; 47 | this.secure = options.secure; 48 | 49 | // TODO my peer state and peer name are modified on start() 50 | // need to refactor so the assignment and declaration is only in one place 51 | 52 | this.peer_name = this._generatePeerName(); 53 | 54 | this.my_state = new PeerState(this.port, this.address); 55 | this.my_state.address = this.address; 56 | this.my_state.port = this.port; 57 | this.listenToExpiredKeys(this.my_state); 58 | 59 | this.peers = {}; 60 | 61 | // TODO: consider removing my_state from peers 62 | // thus eliminating checks for self in various peer iterations 63 | this.peers[this.peer_name] = this.my_state; 64 | 65 | this.beatHeart = true; 66 | this.emitUpdateOnHeartBeat = options.emitUpdateOnHeartBeat || false; 67 | this.scuttle = new Scuttle(this.peers); 68 | 69 | this.seeds = options.seeds || []; 70 | 71 | for (var i = 0; i < this.seeds.length; i++) { 72 | if (this.seeds[i] === this.peer_name) 73 | throw new Error('cannot specify self as seed') 74 | } 75 | 76 | // TODO: another ugly hack :( 77 | var seeds = {}; 78 | 79 | for (var i = 0; i < this.seeds.length; i++) 80 | seeds[this.seeds[i]] = undefined; 81 | 82 | this.handleNewPeers(seeds); 83 | 84 | // hook these two to socket events on both incoming and outgoing connections: 85 | var self = this; 86 | 87 | this._onSocketData = function (message, socket) { 88 | var reply = self.handleMessage(message); 89 | 90 | if (reply) { 91 | socket.write(reply); 92 | } 93 | 94 | if (message.type !== Gossiper.REQUEST) 95 | socket.end(); 96 | }; 97 | 98 | this._onSocketError = function (e) { 99 | debug('socket error: %s', e); 100 | }; 101 | } 102 | 103 | Gossiper.prototype._generatePeerName = function() { 104 | if (net.isIPv6(this.address)) { 105 | return '[' + this.address + ']' + ':' + this.port; 106 | } 107 | 108 | return this.address + ':' + this.port; 109 | } 110 | 111 | Gossiper.prototype.start = function(callback) { 112 | 113 | var self = this; 114 | 115 | this.server = this._newServerAdapter(); 116 | 117 | this.server.on('listening', start); 118 | 119 | this.server.on('connection', onConnection); 120 | 121 | this.server.on('error', onError); 122 | 123 | this.server.listen(this.port, this.address); 124 | 125 | if (this.beatHeart) { 126 | this.heartBeatTimer = setInterval(function() { 127 | self.my_state.beatHeart(); 128 | }, 1000); 129 | } 130 | 131 | this.gossipTimer = setInterval(function() { 132 | self.gossip(); 133 | }, 1000); 134 | 135 | function onConnection(socket) { 136 | socket.on('data', self._onSocketData); 137 | socket.on('error', self._onSocketError); 138 | } 139 | 140 | function onError(e) { 141 | debug(e) 142 | } 143 | 144 | function start(err) { 145 | debug('%s started', self.peer_name); 146 | 147 | if (callback) 148 | callback(err, self); 149 | 150 | self.started = true; 151 | self.emit('started', self); 152 | } 153 | } 154 | 155 | Gossiper.prototype.stop = function(callback) { 156 | clearInterval(this.heartBeatTimer); 157 | clearInterval(this.gossipTimer); 158 | var self = this; 159 | if (this.server) { 160 | 161 | this.server.once('close', function () { 162 | self.server = undefined 163 | self.started = false; 164 | callback(); 165 | }); 166 | 167 | this.server.close(); 168 | } else { 169 | this.started = false; 170 | setImmediate(callback); 171 | } 172 | } 173 | 174 | // The method of choosing which peer(s) to gossip to is borrowed from Cassandra. 175 | // They seemed to have worked out all of the edge cases 176 | // http://wiki.apache.org/cassandra/ArchitectureGossip 177 | Gossiper.prototype.gossip = function() { 178 | this.emit('gossip start'); 179 | 180 | var now = Date.now(); 181 | 182 | for (var p in this.peers) 183 | this.peers[p].expireLocalKeys(now); 184 | 185 | var livePeers = this.livePeers(); 186 | 187 | // Find a live peer to gossip to 188 | var livePeer; 189 | 190 | if (livePeers.length > 0) { 191 | livePeer = this.chooseRandom(livePeers); 192 | this.gossipToPeer(livePeer); 193 | } 194 | 195 | var deadPeers = this.deadPeers(); 196 | 197 | // Possilby gossip to a dead peer 198 | var prob = deadPeers.length / (livePeers.length + 1) 199 | if (Math.random() < prob) { 200 | var deadPeer = this.chooseRandom(deadPeers); 201 | this.gossipToPeer(deadPeer); 202 | } 203 | 204 | //TODO this following comment is from the original fork, i dont understand 205 | //why it says "gossip to seed" but chooses a peer from all the peers 206 | // Gossip to seed under certain conditions 207 | if (livePeer && !this.seeds[livePeer] && livePeers.length < this.seeds.length) { 208 | if (Math.random() < (this.seeds / this.peers.length)) { 209 | var p = this.chooseRandom(this.allPeers()) 210 | this.gossipToPeer(p); 211 | } 212 | } 213 | 214 | // Check health of peers 215 | for (var i in this.peers) { 216 | var peer = this.peers[i]; 217 | if (peer !== this.my_state) { 218 | peer.isSuspect(); 219 | } 220 | } 221 | } 222 | 223 | Gossiper.prototype.chooseRandom = function(peers) { 224 | // Choose random peer to gossip to 225 | var i = Math.floor(Math.random() * 1000000) % peers.length; 226 | return this.peers[peers[i]]; 227 | } 228 | 229 | Gossiper.prototype.gossipToPeer = function(peer) { 230 | 231 | if (debug.enabled) { 232 | debug('%s => %s', this.peer_name, peer.name); 233 | } 234 | 235 | var socket = this._newSocketAdapter(); 236 | 237 | socket.on('data', this._onSocketData); 238 | 239 | var self = this 240 | socket.on('connect', function() { 241 | socket.write(self.requestMessage()); 242 | self.emit('gossip', peer); 243 | }) 244 | 245 | socket.on('error', this._onSocketError); 246 | 247 | socket.connect(peer.port, peer.address); 248 | } 249 | 250 | Gossiper.REQUEST = 0; 251 | Gossiper.FIRST_RESPONSE = 1; 252 | Gossiper.SECOND_RESPONSE = 2; 253 | 254 | Gossiper.prototype.handleMessage = function(msg) { 255 | 256 | switch (msg.type) { 257 | // the request message is from the connecting client and is handled at the server peer 258 | case Gossiper.REQUEST: 259 | return this.firstResponseMessage(msg.digest, msg.psk); 260 | 261 | // the first response message is from the server peer and is handled by the client peer 262 | case Gossiper.FIRST_RESPONSE: 263 | this.scuttle.updateKnownState(msg.updates); 264 | return this.secondResponseMessage(msg.request_digest); 265 | 266 | // the second response message is from the connecting client and is handled at the server peer 267 | case Gossiper.SECOND_RESPONSE: 268 | this.scuttle.updateKnownState(msg.updates); 269 | break; 270 | 271 | default: 272 | debug('unknown message type', msg.type) 273 | break; 274 | } 275 | } 276 | 277 | // MESSSAGES 278 | Gossiper.prototype.handleNewPeers = function(newPeers) { 279 | var self = this; 280 | for (var p in newPeers) { 281 | var peer_info; 282 | // TODO can this be done without regex? 283 | var m = p.match(/\[(.+)\]:([0-9]+)/); 284 | var address; 285 | var port; 286 | 287 | if (m) { 288 | address = m[1]; 289 | port = m[2]; 290 | } else { 291 | m = p.split(':'); 292 | address = m[0]; 293 | port = m[1]; 294 | } 295 | 296 | var tp = new PeerState(parseInt(port), address); 297 | 298 | tp.metadata = newPeers[p] 299 | 300 | this.peers[tp.name] = tp; 301 | 302 | this.emit('new_peer', tp); 303 | 304 | this.listenToPeer(tp); 305 | } 306 | } 307 | 308 | Gossiper.prototype.listenToPeer = function(peer) { 309 | var self = this; 310 | 311 | if (peer.name === this.peer_name) 312 | throw new Error('cannot listen to itself') 313 | 314 | var peerName = peer.name; 315 | 316 | this.listenToExpiredKeys(peer) 317 | 318 | peer.on('update', function(k, v, expires) { 319 | 320 | if (k !== '__heartbeat__') 321 | self.emit('update', peerName, k, v, expires); 322 | else if (self.emitUpdateOnHeartBeat) 323 | self.emit('update', peerName, k, v, expires); // heartbeats are disabled by default but it can be changed so this takes care of that 324 | }); 325 | 326 | peer.on('peer_alive', function() { 327 | self.emit('peer_alive', peerName); 328 | }); 329 | 330 | peer.on('peer_failed', function() { 331 | self.emit('peer_failed', peerName); 332 | }); 333 | } 334 | 335 | Gossiper.prototype.listenToExpiredKeys = function(peer) { 336 | 337 | var self = this; 338 | peer.on('expire', function(k, v, expires) { 339 | self.emit('expire', peer.name, k, v, expires); 340 | }); 341 | } 342 | 343 | Gossiper.prototype.requestMessage = function() { 344 | var m = { 345 | type: Gossiper.REQUEST, 346 | digest: this.scuttle.digest() 347 | }; 348 | 349 | if (this.secure && this.presharedKey) { 350 | debug('send request message with preshared key') 351 | m.psk = this.presharedKey; 352 | } 353 | 354 | return m; 355 | }; 356 | 357 | Gossiper.prototype.firstResponseMessage = function(peer_digest, psk) { 358 | // if we are secure and the psk is not the same as our, return an empty message 359 | // do not discover new peers from this peer and so on 360 | if (this.secure && this.presharedKey !== psk) { 361 | debug('Unauthorized peer!') 362 | return {} 363 | } 364 | 365 | var sc = this.scuttle.scuttle(peer_digest) 366 | 367 | this.handleNewPeers(sc.new_peers) 368 | 369 | var m = { 370 | type: Gossiper.FIRST_RESPONSE, 371 | request_digest: sc.requests, 372 | updates: sc.deltas 373 | }; 374 | 375 | return m; 376 | }; 377 | 378 | Gossiper.prototype.secondResponseMessage = function(requests) { 379 | var m = { 380 | type: Gossiper.SECOND_RESPONSE, 381 | updates: this.scuttle.fetchDeltas(requests) 382 | }; 383 | return m; 384 | }; 385 | 386 | Gossiper.prototype.setLocalState = function(k, v, expires) { 387 | this.my_state.updateLocal(k, v, expires); 388 | } 389 | 390 | Gossiper.prototype.getLocalState = function(k) { 391 | return this.my_state.getValue(k); 392 | } 393 | 394 | Gossiper.prototype.peerKeys = function(peer) { 395 | if (!peer) throw new Error('must specify a peer') 396 | return this.peers[peer].getKeys(); 397 | } 398 | 399 | Gossiper.prototype.peerValue = function(peer, k) { 400 | if (!peer) throw new Error('must specify a peer') 401 | if (!k) throw new Error('must specify a key') 402 | 403 | return this.peers[peer].getValue(k); 404 | } 405 | 406 | Gossiper.prototype.allPeers = function() { 407 | var keys = []; 408 | for (var k in this.peers) { 409 | var peer = this.peers[k]; 410 | if (peer !== this.my_state) 411 | keys.push(k) 412 | } 413 | return keys; 414 | } 415 | 416 | Gossiper.prototype.livePeers = function() { 417 | var keys = []; 418 | 419 | for (var k in this.peers) { 420 | var peer = this.peers[k]; 421 | if (peer !== this.my_state && peer.alive) { 422 | keys.push(k) 423 | } 424 | } 425 | 426 | return keys; 427 | } 428 | 429 | Gossiper.prototype.deadPeers = function() { 430 | var keys = []; 431 | 432 | for (var k in this.peers) { 433 | var peer = this.peers[k]; 434 | if (peer !== this.my_state && !peer.alive) { 435 | keys.push(k) 436 | } 437 | } 438 | 439 | return keys; 440 | } 441 | -------------------------------------------------------------------------------- /lib/peer_state.js: -------------------------------------------------------------------------------- 1 | var AccrualFailureDetector = require('./accrual_failure_detector'); 2 | var EventEmitter = require('events').EventEmitter; 3 | var util = require('util'); 4 | var net = require('net'); 5 | var debug = require('debug')('grapevine_PeerState') 6 | 7 | module.exports = PeerState; 8 | 9 | function PeerState(port, address) { 10 | EventEmitter.call(this); 11 | 12 | if (typeof port !== 'number' || port === 0) 13 | throw new Error('must specify a port'); 14 | 15 | this.max_version_seen = 0; 16 | this.attrs = {}; 17 | this.detector = new AccrualFailureDetector(); 18 | this.alive = true; 19 | this.heart_beat_version = 0; 20 | this.PHI = 8; 21 | this.address = address || '127.0.0.1'; 22 | this.port = port; 23 | this.minExpiresSeen = Infinity; 24 | }; 25 | 26 | util.inherits(PeerState, EventEmitter); 27 | 28 | //TODO: why is this dynamic ? 29 | Object.defineProperty(PeerState.prototype, 'name', { 30 | get: function() { 31 | if(net.isIPv6(this.address)) { 32 | return ['[' + this.address + ']', this.port.toString()].join(':'); 33 | } 34 | return [this.address, this.port.toString()].join(':'); 35 | } 36 | ,enumerable: true 37 | }); 38 | 39 | PeerState.prototype.updateWithDelta = function(k, v, n, expires, now) { 40 | // It's possibly to get the same updates more than once if we're gossiping with multiple peers at once 41 | // ignore them, also ignore updates that have expired 42 | if(n > this.max_version_seen) { 43 | if (typeof(expires) === 'number' && expires < now) { 44 | return; 45 | } 46 | 47 | this.max_version_seen = n; 48 | this.setKey(k, v, n, expires); 49 | 50 | if(k == '__heartbeat__') { 51 | var d = new Date(); 52 | this.detector.add(d.getTime()); 53 | } 54 | } 55 | } 56 | 57 | /* This is used when the peerState is owned by this peer */ 58 | PeerState.prototype.updateLocal = function(k, v, expires) { 59 | this.max_version_seen += 1; 60 | this.setKey(k, v, this.max_version_seen, expires); 61 | } 62 | 63 | PeerState.prototype.getValue = function(k) { 64 | if(this.attrs[k] == undefined) { 65 | return undefined; 66 | } else { 67 | return this.attrs[k][0]; 68 | } 69 | } 70 | 71 | PeerState.prototype.getKeys = function() { 72 | var keys = []; 73 | for(k in this.attrs) { keys.push(k) }; 74 | return keys; 75 | } 76 | 77 | PeerState.prototype.setKey = function(k, v, n, expires) { 78 | // update min expires if needed 79 | if (typeof(expires) === 'number' && expires < this.minExpiresSeen) { 80 | debug('updating minExpiresSeen %s => %s', this.minExpiresSeen, expires) 81 | this.minExpiresSeen = expires 82 | } 83 | 84 | this.attrs[k] = [v, n, expires]; 85 | this.emitUpdate(k, v, expires) 86 | } 87 | 88 | PeerState.prototype.emitUpdate = function(k, v, expires) { 89 | 90 | var self = this 91 | setImmediate(function () { 92 | self.emit('update', k, v, expires); 93 | }) 94 | } 95 | 96 | PeerState.prototype.expireLocalKeys = function(now) { 97 | // nothing to do, next expire is still in the future 98 | if (now < this.minExpiresSeen) 99 | return; 100 | 101 | if (debug.enabled) { 102 | debug('%s expireLocalKeys()', this.name); 103 | } 104 | 105 | // else: now >= this.minExpiresSeen 106 | 107 | var minExpires = Infinity 108 | 109 | for (var k in this.attrs) { 110 | 111 | var entry = this.attrs[k]; 112 | var expires = entry[2]; 113 | 114 | if (typeof(expires) !== 'number') continue; 115 | 116 | if (expires <= now) { 117 | this.expireKey(k); 118 | 119 | // from all the non expiring keys, find the next min expires 120 | } else if (expires < minExpires) { 121 | minExpires = expires 122 | } 123 | } 124 | 125 | this.minExpiresSeen = minExpires 126 | } 127 | 128 | PeerState.prototype.expireKey = function(k) { 129 | 130 | var value = this.attrs[k][0] 131 | var expires = this.attrs[k][2] 132 | 133 | delete this.attrs[k] 134 | var self = this 135 | setImmediate(function () { 136 | if (debug.enabled) 137 | debug('%s: expiring %s', self.name, k) 138 | 139 | self.emit('expire', k, value, expires); 140 | }) 141 | } 142 | 143 | PeerState.prototype.beatHeart = function() { 144 | this.heart_beat_version += 1; 145 | this.updateLocal('__heartbeat__', this.heart_beat_version); 146 | } 147 | 148 | PeerState.prototype.deltasAfterVersion = function(lowest_version) { 149 | var deltas = [] 150 | 151 | for(k in this.attrs) { 152 | var value = this.attrs[k][0]; 153 | var version = this.attrs[k][1]; 154 | var expires = this.attrs[k][2]; 155 | 156 | if(version > lowest_version) { 157 | deltas.push([k,value,version,expires]); 158 | } 159 | } 160 | 161 | return deltas; 162 | } 163 | 164 | PeerState.prototype.isSuspect = function() { 165 | var d = new Date(); 166 | var phi = this.detector.phi(d.getTime()); 167 | if(phi > this.PHI) { 168 | this.markDead(); 169 | return true; 170 | } else { 171 | this.markAlive(); 172 | return false; 173 | } 174 | } 175 | 176 | PeerState.prototype.markAlive = function() { 177 | if(!this.alive) { 178 | this.alive = true; 179 | this.emit('peer_alive'); 180 | } 181 | } 182 | 183 | PeerState.prototype.markDead = function() { 184 | if(this.alive) { 185 | this.alive = false; 186 | this.emit('peer_failed'); 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /lib/scuttle.js: -------------------------------------------------------------------------------- 1 | var debug = require('debug')('grapevine_scuttle'); 2 | 3 | var PeerState = require('./peer_state'); 4 | 5 | module.exports = Scuttle; 6 | 7 | function Scuttle (peers) { 8 | this.peers = peers; 9 | }; 10 | 11 | Scuttle.prototype.digest = function() { 12 | var digest = {}; 13 | 14 | for(i in this.peers) { 15 | var p = this.peers[i]; 16 | digest[i] = { 17 | maxVersionSeen: p.max_version_seen, 18 | metadata: p.metadata 19 | } 20 | } 21 | return digest; 22 | } 23 | 24 | // HEART OF THE BEAST 25 | 26 | Scuttle.prototype.scuttle = function(digest) { 27 | var deltas_with_peer = []; 28 | var requests = {}; 29 | var new_peers = {}; 30 | 31 | for(var peer in digest) { 32 | var localVersion = this.maxVersionSeenForPeer(peer); 33 | var localPeer = this.peers[peer]; 34 | var digestVersion = digest[peer].maxVersionSeen; 35 | 36 | if(!this.peers[peer]) { 37 | // We don't know about this peer. Request all information. 38 | requests[peer] = 0; 39 | new_peers[peer] = digest.metadata; 40 | } else if(localVersion > digestVersion) { 41 | // We have more recent information for this peer. Build up deltas. 42 | deltas_with_peer.push( { peer : peer, deltas : localPeer.deltasAfterVersion(digestVersion) }); 43 | } else if(localVersion < digestVersion) { 44 | // They have more recent information, request it. 45 | requests[peer] = localVersion; 46 | } else { 47 | // Everything is the same. 48 | } 49 | } 50 | 51 | // Sort by peers with most deltas 52 | deltas_with_peer.sort( function(a,b) { return b.deltas.length - a.deltas.length } ); 53 | 54 | var deltas = []; 55 | for(var i = 0; i < deltas_with_peer.length; i++) { 56 | var peer = deltas_with_peer[i]; 57 | var peer_deltas = peer.deltas; 58 | 59 | // Sort deltas by version number 60 | peer_deltas.sort(function(a,b) { return a[2] - b[2]; }); 61 | 62 | if(peer_deltas.length > 1) { 63 | debug(peer_deltas); 64 | } 65 | 66 | //TODO: possible optimization: dont use unshift 67 | for(var j = 0; j < peer_deltas.length; j++) { 68 | var delta = peer_deltas[j]; 69 | delta.unshift(peer.peer); 70 | deltas.push(delta); 71 | } 72 | } 73 | 74 | return { 'deltas' : deltas, 75 | 'requests' : requests, 76 | 'new_peers' : new_peers }; 77 | } 78 | 79 | Scuttle.prototype.maxVersionSeenForPeer = function(peer) { 80 | if(this.peers[peer]) { 81 | return this.peers[peer].max_version_seen; 82 | } else { 83 | return 0; 84 | } 85 | } 86 | 87 | Scuttle.prototype.updateKnownState = function(deltas) { 88 | var now = Date.now(); 89 | for(i in deltas) { 90 | var d = deltas[i]; 91 | var peer_name = d.shift(); 92 | var peer_state = this.peers[peer_name]; 93 | peer_state.updateWithDelta(d[0],d[1],d[2],d[3],now); 94 | } 95 | }; 96 | 97 | Scuttle.prototype.fetchDeltas = function(requests) { 98 | var deltas = [] 99 | for(i in requests) { 100 | var peer_deltas = this.peers[i].deltasAfterVersion(requests[i]); 101 | peer_deltas.sort(function(a,b) { return a[2] - b[2]; }); 102 | for(var j = 0; j < peer_deltas.length; j++) { 103 | peer_deltas[j].unshift(i); 104 | deltas.push(peer_deltas[j]); 105 | } 106 | } 107 | return deltas; 108 | } 109 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "grapevine", 3 | "description": "gossip protocol - a fork of gossiper module with some added features", 4 | "version": "1.0.6", 5 | "author": "Bob Potter ", 6 | "contributors": [ 7 | { 8 | "name": "Christopher Mooney", 9 | "email": "chris@dod.net" 10 | }, 11 | { 12 | "name": "Yaniv Kessler", 13 | "email": "yanivk@gmail.com" 14 | }, 15 | { 16 | "name": "Joshua Erickson", 17 | "email": "josh@snoj.us" 18 | } 19 | ], 20 | "repository": { 21 | "type": "git", 22 | "url": "git://github.com/kessler/node-gossip.git" 23 | }, 24 | "keywords": [ 25 | "gossip", 26 | "scuttlebutt" 27 | ], 28 | "dependencies": { 29 | "debug": "~0.7.4", 30 | "emitter-sniffer": "0.0.1", 31 | "nssocket": "^0.5.2", 32 | "pem": "^1.4.1" 33 | }, 34 | "main": "index.js", 35 | "scripts": { 36 | "test": "mocha -R spec" 37 | }, 38 | "engines": { 39 | "node": "0.8.x || 0.9.x || 0.10.x || 0.12.x" 40 | }, 41 | "devDependencies": { 42 | "async": "^0.9.0", 43 | "mocha": "^2.2.4", 44 | "should": "^4.3.0" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /simulation/example.js: -------------------------------------------------------------------------------- 1 | var Gossiper = require('../lib/gossiper'); 2 | // Create a seed peer. 3 | var seed = new Gossiper({ port: 9000, seeds:[], address: '127.0.0.1' }); 4 | seed.start(); 5 | 6 | // Create 20 new peers and point them at the seed (usually this would happen in 20 separate processes) 7 | // To prevent having a single point of failure you would probably have multiple seeds 8 | for(var i = 9001; i <= 9020;i++) { 9 | var g = new Gossiper({ port: i, seeds: ['127.0.0.1:9000'] }); 10 | g.start(); 11 | console.log(i) 12 | 13 | g.on('update', function(peer, k, v) { 14 | if(k == 'somekey') { 15 | console.log("peer ", peer, " set ", k, " to ", v); // peer 127.0.0.1:9999 set somekey to somevalue 16 | } 17 | }); 18 | 19 | sendUpdate(g); 20 | } 21 | 22 | // Add another peer which updates it's state after 15 seconds 23 | var updater = new Gossiper({ port: 9999, seeds: ['127.0.0.1:9000'] }); 24 | updater.start(); 25 | sendUpdate(updater); 26 | 27 | var time = 30000; 28 | 29 | function sendUpdate(g) { 30 | time = time - 100; 31 | setTimeout(function() { 32 | g.setLocalState('somekey', {x:1, y:2}); 33 | }, time); 34 | } 35 | -------------------------------------------------------------------------------- /simulation/s1.js: -------------------------------------------------------------------------------- 1 | var Gossiper = require('../lib/gossiper'); 2 | 3 | var seed = new Gossiper({ port: 9000, seeds: [] }); 4 | seed.start(); 5 | 6 | var n = 0; 7 | var gs = []; 8 | var start_time = undefined; 9 | var count = 100; 10 | for(var i = 9001; i < 9001+count;i++) { 11 | var g = gs[i] = new Gossiper({ port: i, seeds: ['127.0.0.1:9000'] }); 12 | g.start(); 13 | g.on('update', function(peer,k,v) { 14 | if(k == "hi") { 15 | console.log("hi received by " + this.peer_name + " at " + (new Date().getTime())); 16 | n++; 17 | if(n == count) { 18 | console.log("fully propogated"); 19 | console.log("took " + (new Date().getTime() - start_time)); 20 | process.exit(); 21 | } 22 | } 23 | }); 24 | } 25 | 26 | var g = new Gossiper({ port: 9999, seeds: ['127.0.0.1:9000'] }); 27 | g.start(); 28 | 29 | setTimeout(function() { 30 | console.log(seed.allPeers()); 31 | // Set value for 'hi' 32 | g.setLocalState('hi', 'hello'); 33 | start_time = new Date().getTime(); 34 | console.log('hi sent ' + (new Date().getTime())); 35 | }, 10000); 36 | -------------------------------------------------------------------------------- /simulation/s2.js: -------------------------------------------------------------------------------- 1 | var Gossiper = require('../lib/gossiper'); 2 | 3 | var seed = new Gossiper({ port: 9000, seeds: [] }); 4 | seed.start(); 5 | 6 | var n = 0; 7 | var gs = []; 8 | var start_time = undefined; 9 | var count = 100; 10 | var setup_peer = function(this_peer) { 11 | this_peer.start(); 12 | this_peer.on('peer_failed', function(peer) { 13 | console.log(this_peer.peer_name + " thinks " + peer + " is dead"); 14 | }); 15 | this_peer.on('peer_alive', function(peer) { 16 | console.log(this_peer.peer_name + " thinks " + peer + " is alive"); 17 | }); 18 | } 19 | for(var i = 9001; i < 9001+count;i++) { 20 | var g = gs[i] = new Gossiper({ port: i, seeds: ['127.0.0.1:9000'] }); 21 | setup_peer(g); 22 | } 23 | // kill one of the nodes 24 | setTimeout(function() { 25 | gs[9020].stop(); 26 | setTimeout(function() { gs[9020].start() }, 30000); 27 | }, 5000); 28 | -------------------------------------------------------------------------------- /simulation/s3.js: -------------------------------------------------------------------------------- 1 | var Gossiper = require('../lib/gossiper') 2 | 3 | var seed1 = new Gossiper({ port: 9000, seeds: [] }); 4 | seed1.start(); 5 | 6 | var seed2 = new Gossiper({ port: 9001, seeds: [] }); 7 | seed2.start(); 8 | 9 | var n = 0; 10 | var gs = []; 11 | var count = 100; 12 | var peers_done = 0; 13 | var setup_peer = function(this_peer) { 14 | var n = 0; 15 | this_peer.on('new_peer', function() { 16 | n++; 17 | if(n == 100) { 18 | console.log('peer done'); 19 | peers_done++; 20 | if(peers_done == 100) { 21 | console.log("all peers know about each other"); 22 | process.exit(); 23 | } 24 | } 25 | }); 26 | } 27 | 28 | for(var i = 9101; i <= 9101+count;i++) { 29 | var g = gs[i] = new Gossiper({ port: i, seeds: ['127.0.0.1:9000', '127.0.0.1:9001'] }); 30 | setup_peer(g); 31 | g.start(); 32 | } 33 | -------------------------------------------------------------------------------- /test/NetworkAdapters.test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert') 2 | var SocketAdapter = require('../lib/SocketAdapter.js') 3 | var ServerAdapter = require('../lib/ServerAdapter.js') 4 | var should = require('should') 5 | 6 | describe('Network adapters', function () { 7 | 8 | var SERVER_PORT = 4321 9 | 10 | var socket, server 11 | 12 | describe('ServerAdapter', function () { 13 | it('emits a connection event', function (done) { 14 | server.on('connection', function (s) { 15 | s.should.be.an.instanceof(SocketAdapter) 16 | done() 17 | }) 18 | 19 | socket.connect(SERVER_PORT) 20 | }) 21 | }) 22 | 23 | describe('SocketAdapter', function () { 24 | it('connects to a server', function (done) { 25 | socket.once('connect', done) 26 | socket.connect(SERVER_PORT) 27 | }) 28 | 29 | it('emits a data event', function (done) { 30 | server.on('connection', function (s) { 31 | s.write({ test: 123 }) 32 | }) 33 | 34 | socket.on('data', function (message) { 35 | message.should.eql({ test: 123 }) 36 | done() 37 | }) 38 | 39 | socket.connect(SERVER_PORT) 40 | }) 41 | }) 42 | 43 | beforeEach(function (done) { 44 | socket = new SocketAdapter() 45 | server = new ServerAdapter() 46 | server.once('listening', done) 47 | server.listen(SERVER_PORT) 48 | }) 49 | 50 | afterEach(function (done) { 51 | server.once('close', done) 52 | socket.end() 53 | server.close() 54 | }) 55 | }) 56 | -------------------------------------------------------------------------------- /test/accrual_failure_detector.test.js~: -------------------------------------------------------------------------------- 1 | var AccrualFailureDetector = require('../lib/accrual_failure_detector'); 2 | 3 | module.exports = { 4 | 'should have a low phi value after only a second' : function(beforeExit, assert) { 5 | var afd = new AccrualFailureDetector(); 6 | var time = 0; 7 | for(var i = 0;i < 100;i++) { 8 | time += 1000; 9 | afd.add(time); 10 | } 11 | assert.ok(afd.phi(time + 1000) < 0.5); 12 | }, 13 | 14 | 'should have a high phi value after ten seconds' : function(beforeExit, assert) { 15 | var afd = new AccrualFailureDetector(); 16 | var time = 0; 17 | for(var i = 0;i < 100;i++) { 18 | time += 1000; 19 | afd.add(time); 20 | } 21 | assert.ok(afd.phi(time + 10000) > 4); 22 | }, 23 | 24 | 'should only keep last 1000 values' : function(beforeExit, assert) { 25 | var afd = new AccrualFailureDetector(); 26 | var time = 0; 27 | for(var i = 0;i < 2000;i++) { 28 | time += 1000; 29 | afd.add(time); 30 | } 31 | assert.equal(1000, afd.intervals.length); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /test/gossiper.real.test.js: -------------------------------------------------------------------------------- 1 | var should = require('should') 2 | var Gossiper = require('../lib/gossiper') 3 | var inspect = require('util').inspect 4 | var async = require('async') 5 | 6 | describe('gossiper', function() { 7 | var beforeEachDelay = 5000 8 | var seed, g1, g2 9 | 10 | it('discovers new peers', function() { 11 | should(g1.allPeers()).eql(['127.0.0.1:7000', '127.0.0.1:7002']) 12 | should(g2.allPeers()).eql(['127.0.0.1:7000', '127.0.0.1:7001']) 13 | should(seed.allPeers()).eql(['127.0.0.1:7001', '127.0.0.1:7002']) 14 | }) 15 | 16 | it('propagates keys and values between peers', function(done) { 17 | this.timeout(4000) 18 | 19 | g1.setLocalState('x', 'y') 20 | 21 | setTimeout(function () { 22 | g2.peerValue('127.0.0.1:7001', 'x').should.eql('y') 23 | seed.peerValue('127.0.0.1:7001', 'x').should.eql('y') 24 | done() 25 | }, 3000) 26 | }) 27 | 28 | it('expires keys with ttl throughout the network', function (done) { 29 | this.timeout(7000) 30 | g1.setLocalState('x', 'y', Date.now() + 4000) 31 | 32 | var expiredEvents = 0 33 | 34 | g1.on('expire', function(peer, k, v, expire) { 35 | peer.should.be.eql('127.0.0.1:7001') 36 | k.should.be.eql('x') 37 | expiredEvents++ 38 | }) 39 | 40 | g2.on('expire', function(peer, k, v, expire) { 41 | peer.should.be.eql('127.0.0.1:7001') 42 | k.should.be.eql('x') 43 | expiredEvents++ 44 | }) 45 | 46 | seed.on('expire', function(peer, k, v, expire) { 47 | peer.should.be.eql('127.0.0.1:7001') 48 | k.should.be.eql('x') 49 | expiredEvents++ 50 | }) 51 | 52 | setTimeout(function () { 53 | g2.peerValue('127.0.0.1:7001', 'x').should.eql('y') 54 | seed.peerValue('127.0.0.1:7001', 'x').should.eql('y') 55 | setTimeout(function () { 56 | g2.peerKeys('127.0.0.1:7001').should.not.containEql('x') 57 | seed.peerKeys('127.0.0.1:7001').should.not.containEql('x') 58 | expiredEvents.should.be.eql(3) 59 | done() 60 | }, 2000) 61 | }, 2500) 62 | }) 63 | 64 | it('knows when peers die or come back alive', function (done) { 65 | this.timeout(125000) 66 | 67 | var g2Stopped = false 68 | var g2Started = false 69 | 70 | var peerFail = { g1: false, seed: false } 71 | var peerAlive = { g1: false, seed: false } 72 | 73 | g1.on('peer_failed', function(peer) { 74 | peer.should.be.eql('127.0.0.1:7002') 75 | peerFail.g1 = true 76 | }) 77 | 78 | seed.on('peer_failed', function(peer) { 79 | peer.should.be.eql('127.0.0.1:7002') 80 | peerFail.seed = true 81 | }) 82 | 83 | g1.on('peer_alive', function(peer) { 84 | peer.should.be.eql('127.0.0.1:7002') 85 | peerAlive.g1 = true 86 | }) 87 | 88 | seed.on('peer_alive', function (peer) { 89 | peer.should.be.eql('127.0.0.1:7002') 90 | peerAlive.seed = true 91 | }) 92 | 93 | g2.stop(function () { 94 | g2Stopped = true 95 | }) 96 | 97 | // check for peer failed event after 35 seconds 98 | setTimeout(function () { 99 | g2Stopped.should.be.true 100 | peerFail.g1.should.be.true 101 | peerFail.seed.should.be.true 102 | 103 | g2.start(function () { 104 | g2Started = true 105 | }) 106 | 107 | // check for peer alive event after 35 seconds 108 | setTimeout(function () { 109 | g2Started.should.be.true 110 | peerAlive.g1.should.be.true 111 | peerAlive.seed.should.be.true 112 | done() 113 | }, 35000) 114 | 115 | }, 35000) 116 | }) 117 | 118 | it('randomly gossips with peers', function (done) { 119 | this.timeout(12000) 120 | // this is not exactly a statistical test, but its something... 121 | 122 | var seedGossip = [] 123 | seed.on('gossip', function(peer) { 124 | seedGossip.push(peer.port) 125 | }) 126 | 127 | var g1Gossip = [] 128 | g1.on('gossip', function(peer) { 129 | g1Gossip.push(peer.port) 130 | }) 131 | 132 | var g2Gossip = [] 133 | g2.on('gossip', function(peer) { 134 | g2Gossip.push(peer.port) 135 | }) 136 | 137 | setTimeout(function () { 138 | seedGossip.should.containEql(7001) 139 | seedGossip.should.containEql(7002) 140 | 141 | g1Gossip.should.containEql(7000) 142 | g1Gossip.should.containEql(7002) 143 | 144 | g2Gossip.should.containEql(7000) 145 | g2Gossip.should.containEql(7001) 146 | 147 | done() 148 | }, 8000) 149 | }) 150 | 151 | it('emits an update event when peers propogate data', function (done) { 152 | this.timeout(7000) 153 | 154 | g2.on('update', function (peer, k, v, expiry) { 155 | peer.should.be.eql('127.0.0.1:7001') 156 | k.should.be.eql('test') 157 | v.should.be.eql({ x: 1}) 158 | done() 159 | }) 160 | 161 | g1.setLocalState('test', { x: 1 }) 162 | }) 163 | 164 | beforeEach(function(done) { 165 | this.timeout(beforeEachDelay + 1000) 166 | 167 | seed = new Gossiper({ port: 7000 }) 168 | g1 = new Gossiper({ port: 7001, seeds: ['127.0.0.1:7000'] }) 169 | g2 = new Gossiper({ port: 7002, seeds: ['127.0.0.1:7000'] }) 170 | 171 | async.parallel([ 172 | function(callback) { 173 | seed.start(callback) 174 | }, 175 | function(callback) { 176 | g1.start(callback) 177 | }, 178 | function(callback) { 179 | g2.start(callback) 180 | } 181 | ], function(err) { 182 | if (err) return done(err) 183 | setTimeout(done, beforeEachDelay) 184 | }) 185 | }) 186 | 187 | afterEach(function(done) { 188 | this.timeout(beforeEachDelay + 1000) 189 | 190 | async.parallel([ 191 | function(callback) { 192 | if (seed.started) { 193 | seed.stop(callback) 194 | } else { 195 | callback() 196 | } 197 | }, 198 | function(callback) { 199 | if (g1.started) { 200 | g1.stop(callback) 201 | } else { 202 | callback() 203 | } 204 | }, 205 | function(callback) { 206 | if (g2.started) { 207 | g2.stop(callback) 208 | } else { 209 | callback() 210 | } 211 | } 212 | ], done) 213 | }) 214 | }) 215 | -------------------------------------------------------------------------------- /test/gossiper.test.js: -------------------------------------------------------------------------------- 1 | var should = require('should') 2 | var Gossiper = require('../lib/gossiper') 3 | var PeerState = require('../lib/peer_state') 4 | describe('gossiper', function() { 5 | var gossiper 6 | 7 | beforeEach(function(done) { 8 | gossiper = new Gossiper(1234) 9 | done() 10 | }) 11 | 12 | afterEach(function(done) { 13 | gossiper.stop(done) 14 | }) 15 | 16 | it('has local state', function() { 17 | gossiper.setLocalState('hi', 'hello') 18 | gossiper.getLocalState('hi').should.be.eql('hello') 19 | }) 20 | 21 | it('contains a list of keys for each peer', function() { 22 | gossiper.peers.p1 = new PeerState(12345) 23 | gossiper.peers.p1.attrs['keyz'] = [] 24 | gossiper.peers.p1.attrs['keyzy'] = [] 25 | should(gossiper.peerKeys('p1')).eql(['keyz', 'keyzy']) 26 | }) 27 | 28 | it('by default, it remembers values for keys in other peers', function() { 29 | gossiper.peers.p1 = new PeerState(12345) 30 | gossiper.peers.p1.attrs['keyz'] = ['hi', 1] 31 | gossiper.peerValue('p1', 'keyz').should.eql('hi') 32 | }) 33 | 34 | it.skip('does not remember peer key values if told not to do so', function() { 35 | 36 | }) 37 | 38 | it('maintains a list of peers', function() { 39 | gossiper.peers.p1 = new PeerState(12345) 40 | gossiper.peers.p2 = new PeerState(12346) 41 | should(gossiper.allPeers()).eql(['p1', 'p2']) 42 | }) 43 | 44 | it('emits new_peer event when a new peer is discovered', function(done) { 45 | // mock scuttle 46 | gossiper.scuttle = { 47 | scuttle: function(v) { 48 | return { 49 | 'new_peers': ['127.0.0.1:8010'] 50 | } 51 | } 52 | } 53 | 54 | var emitted = false 55 | gossiper.on('new_peer', function(peer) { 56 | peer.metadata.should.be.eql('127.0.0.1:8010') 57 | done() 58 | }) 59 | gossiper.firstResponseMessage({}) 60 | }) 61 | 62 | it('emits an update event when peer sends data', function(done) { 63 | gossiper.peers['127.0.0.1:8010'] = new PeerState(8010) 64 | gossiper.handleNewPeers({ 65 | '127.0.0.1:8010': undefined 66 | }) 67 | 68 | gossiper.on('update', function(peer, k, v, ttl) { 69 | peer.should.eql('127.0.0.1:8010') 70 | k.should.eql('howdy') 71 | v.should.eql('yall') 72 | should(ttl).be.undefined 73 | done() 74 | }) 75 | 76 | gossiper.peers['127.0.0.1:8010'].updateLocal('howdy', 'yall') 77 | }) 78 | 79 | it('new peers have metadata', function() { 80 | gossiper.peers['127.0.0.1:8010'] = new PeerState(8010) 81 | gossiper.handleNewPeers({ 82 | '127.0.0.1:8010': { 83 | data: 1 84 | } 85 | }) 86 | 87 | gossiper.peers['127.0.0.1:8010'].metadata.should.eql({ data: 1 }) 88 | }) 89 | }) 90 | -------------------------------------------------------------------------------- /test/peer_state.test.js~: -------------------------------------------------------------------------------- 1 | var PeerState = require('../lib/peer_state'); 2 | 3 | 4 | module.exports = { 5 | // UpdateWithDelta 6 | 'updateWithDelta should set key to value' : function(beforeExit, assert) { 7 | var ps = new PeerState(1234); 8 | ps.updateWithDelta('a', 'hello', 12); 9 | assert.equal('hello', ps.getValue('a')); 10 | }, 11 | 12 | 'updateWithDelta should update the max version' : function(beforeExit, assert) { 13 | var ps = new PeerState(1234); 14 | ps.updateWithDelta('a', 'hello', 12); 15 | ps.updateWithDelta('a', 'hello', 14); 16 | assert.equal(14, ps.max_version_seen); 17 | }, 18 | 19 | 'updates should trigger \'update\' event' : function(beforeExit, assert) { 20 | var ps = new PeerState(1234); 21 | var n = 0; 22 | ps.on('update', function(k,v) { 23 | ++n; 24 | assert.equal('a', k); 25 | assert.equal('hello', v); 26 | }); 27 | ps.updateWithDelta('a', 'hello', 12); 28 | beforeExit(function() { assert.equal(1, n) }); 29 | }, 30 | 31 | // updateLocal 32 | 'updateLocal should set key to value' : function(beforeExit, assert) { 33 | var ps = new PeerState(1234); 34 | ps.updateLocal('a', 'hello', 12); 35 | assert.equal('hello', ps.getValue('a')); 36 | }, 37 | 38 | 'updateLocal should increment the max version' : function(beforeExit, assert) { 39 | var ps = new PeerState(1234); 40 | ps.updateLocal('a', 'hello'); 41 | ps.updateLocal('a', 'hello'); 42 | assert.equal(2, ps.max_version_seen); 43 | }, 44 | 45 | // deltasAfterVersion 46 | 'deltasAfterVersion should return all deltas after a version number' : function(beforeExit, assert) { 47 | var ps = new PeerState(1234); 48 | ps.updateLocal('a', 1); 49 | ps.updateLocal('b', 'blah'); 50 | ps.updateLocal('a', 'super'); 51 | assert.deepEqual([['a','super','3', undefined]], ps.deltasAfterVersion(2)); 52 | }, 53 | 54 | 'expiring should update minTTLSeen': function(beforeExit, assert) { 55 | var ps = new PeerState(1234); 56 | var now = Date.now(); 57 | 58 | assert.strictEqual(ps.minTTLSeen, Infinity); 59 | 60 | ps.updateLocal('a', 1, now + 1000) 61 | 62 | assert.strictEqual(ps.minTTLSeen, now + 1000) 63 | 64 | ps.updateLocal('b', 1, now + 2000) 65 | 66 | assert.strictEqual(ps.minTTLSeen, now + 1000) 67 | 68 | ps.expireLocalKeys(now + 1000) 69 | 70 | assert.strictEqual(ps.minTTLSeen, now + 2000) 71 | 72 | ps.expireLocalKeys(now + 2000) 73 | 74 | assert.strictEqual(ps.minTTLSeen, Infinity); 75 | 76 | beforeExit(function () { 77 | 78 | }) 79 | }, 80 | 81 | 'expiring should fire an event': function(beforeExit, assert) { 82 | var ps = new PeerState(1234); 83 | 84 | var now = Date.now(); 85 | 86 | assert.strictEqual(ps.minTTLSeen, Infinity); 87 | 88 | ps.updateLocal('a', 1, now + 1000) 89 | 90 | var result; 91 | 92 | ps.on('expire', function(k, v, ttl) { 93 | result = [k, v, ttl]; 94 | }); 95 | 96 | ps.expireLocalKeys(now + 1000); 97 | 98 | beforeExit(function () { 99 | assert.deepEqual(['a', 1, now + 1000], result) 100 | }) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /test/scuttle.test.js~: -------------------------------------------------------------------------------- 1 | var Scuttle = require('../lib/scuttle'); 2 | var PeerState = require('../lib/peer_state'); 3 | 4 | 5 | module.exports = { 6 | // digest 7 | 'digest should have max versions we have seen' : function(beforeExit, assert) { 8 | var p1 = new PeerState(1234); 9 | p1.max_version_seen = 10; 10 | var p2 = new PeerState(1235); 11 | p2.max_version_seen = 12; 12 | var p3 = new PeerState(1236); 13 | p3.max_version_seen = 22; 14 | 15 | var peers = { 16 | a : p1, 17 | b : p2, 18 | c : p3 19 | } 20 | 21 | var scuttle = new Scuttle(peers); 22 | 23 | var expected = { 24 | a: { 25 | maxVersionSeen: 10, 26 | metadata: undefined 27 | }, 28 | b: { 29 | maxVersionSeen: 12, 30 | metadata: undefined 31 | }, 32 | c: { 33 | maxVersionSeen: 22, 34 | metadata: undefined 35 | } 36 | }; 37 | 38 | assert.deepEqual( expected, 39 | scuttle.digest()); 40 | }, 41 | 42 | // scuttle 43 | // scuttle new peer 44 | 'new peers should be in result' : function(beforeExit, assert) { 45 | var scuttle = new Scuttle({}); 46 | var res = scuttle.scuttle( { 'new_peer' : { maxVersionSeen: 12 } } ) 47 | assert.deepEqual( { 'new_peer': undefined }, res.new_peers); 48 | }, 49 | 'request all information about a new peer' : function(beforeExit, assert) { 50 | var scuttle = new Scuttle({}); 51 | var res = scuttle.scuttle( { 'new_peer' : { maxVersionSeen: 12 } } ) 52 | assert.deepEqual({ 'new_peer' : 0}, res.requests); 53 | }, 54 | // scuttle deltas 55 | 'send peer all deltas for peers we know more about' : function(beforeExit, assert) { 56 | var p1 = new PeerState(1234); 57 | p1.updateLocal('hi', 'hello'); 58 | p1.updateLocal('meh', 'goodbye'); 59 | var scuttle = new Scuttle({'me' : p1}); 60 | var res = scuttle.scuttle( {'me' : { maxVersionSeen: 0 }, 'new_peer' : { maxVersionSeen: 12 } } ) 61 | 62 | assert.deepEqual([['me', 'hi', 'hello', 1, undefined], 63 | ['me', 'meh', 'goodbye', 2, undefined]], 64 | res.deltas); 65 | } 66 | 67 | // deltas should be sorted by version number 68 | // deltas should be ordered by the peer with the most 69 | } 70 | -------------------------------------------------------------------------------- /test/secure.real.test.js: -------------------------------------------------------------------------------- 1 | var grapevine = require('../index') 2 | 3 | describe.skip('secure communication', function () { 4 | 5 | var g1, g2 6 | 7 | it('updates', function (done) { 8 | this.timeout(5000) 9 | 10 | g2.on('update', function(peer, k, v, ttl) { 11 | console.log(k, v) 12 | done() 13 | }) 14 | 15 | g1.on('new_peer', function (peer) { 16 | console.log('new peer: %s', peer) 17 | }) 18 | 19 | g2.on('new_peer', function (peer) { 20 | console.log('new peer: %s', peer) 21 | }) 22 | 23 | g1.setLocalState('a', 'b') 24 | }) 25 | 26 | beforeEach(function (done) { 27 | this.timeout(6000) 28 | 29 | // EEK! 30 | grapevine.simpleSecureGossiper({ port: 5001, presharedKey: '123' }, function (err, _g1) { 31 | if (err) return done(err) 32 | g1 = _g1 33 | 34 | g1.start(function (err) { 35 | if (err) return done(err) 36 | 37 | g1.server.on('connection', function (socket) { 38 | console.log(socket) 39 | }) 40 | 41 | grapevine.simpleSecureGossiper({ port: 5002, presharedKey: '123', seeds: ['127.0.0.1:5001'] }, function (err, _g2) { 42 | if (err) return done(err) 43 | g2 = _g2 44 | 45 | g2.start(function(err) { 46 | if (err) return done(err) 47 | 48 | setTimeout(done, 5000) 49 | }) 50 | }) 51 | }) 52 | }) 53 | }) 54 | }) -------------------------------------------------------------------------------- /test/securesocket.js~: -------------------------------------------------------------------------------- 1 | var nssocket = require('nssocket') 2 | var pem = require('pem') 3 | var emitterSniffer = require('emitter-sniffer') 4 | 5 | pem.createCertificate({ 6 | days: 100, 7 | selfSigned: true 8 | }, function(err, keys) { 9 | 10 | var options = {} 11 | //options.secure = true 12 | options.type = 'tls' 13 | options.key = keys.serviceKey 14 | options.cert = keys.certificate 15 | options.ca = keys.certificate 16 | options.requestCert = true 17 | //options.rejectUnauthorized = false 18 | //options.secureProtocol = 'TLSv1_method' 19 | 20 | var server = nssocket.createServer(options, function(socket) { 21 | console.log('incoming connection') 22 | // socket.data(['msg'], function (msg) { 23 | // console.log(msg) 24 | // }) 25 | 26 | // socket.on('error', function (e) { 27 | // console.log(e) 28 | // }) 29 | 30 | socket.send(['server'], { x: 2 }) 31 | socket.on('data', function (d) { 32 | console.log(d) 33 | }) 34 | setTimeout(function () { 35 | console.log('closing') 36 | socket.end() 37 | }, 5000) 38 | }) 39 | 40 | server.on('secureConnection', function (secureSocket) { 41 | console.log('secure connection: ' + secureSocket.authorized) 42 | 43 | secureSocket.on('data', function (msg) { 44 | console.log(msg) 45 | }) 46 | }) 47 | 48 | server.on('clientError', function (e) { 49 | console.log(e) 50 | }) 51 | 52 | server.on('listening', function () { 53 | console.log('listening') 54 | var socket = new nssocket.NsSocket({ type: 'tls', ca: keys.certificate, key: keys.clientKey, cert:keys.certificate }) 55 | 56 | socket.on('error', function (e) { 57 | console.log(e) 58 | }) 59 | 60 | socket.on('start', function () { 61 | console.log('socket connected') 62 | }) 63 | 64 | socket.on('close', function () { 65 | console.log('socket close') 66 | server.close() 67 | }) 68 | 69 | socket.data(['server'], function(msg) { 70 | console.log('message from server: ', msg) 71 | 72 | }) 73 | 74 | socket.connect(2000) 75 | }) 76 | 77 | server.listen(2000) 78 | }) 79 | --------------------------------------------------------------------------------