├── .gitignore ├── .jshintrc ├── .travis.yml ├── README.md ├── Vagrantfile ├── lib ├── default_options.js ├── node.js ├── peer.js ├── server.js └── transport.js ├── package.json └── tests ├── _debug.js ├── compatibility.js ├── gossip.js ├── networked.js └── standalone.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | npm-debug.log 4 | coverage 5 | db 6 | .vagrant 7 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "boss": true, 3 | "node": true, 4 | "strict": true, 5 | "white": true, 6 | "smarttabs": true, 7 | "maxlen": 80, 8 | "newcap": false, 9 | "undef": true, 10 | "unused": true, 11 | "onecase": true, 12 | "indent": 2, 13 | "sub": true 14 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sombrero Node 2 | 3 | [![Build Status](https://travis-ci.org/sombrerohq/sombrero-node.svg)](https://travis-ci.org/sombrerohq/sombrero-node) 4 | [![Dependency Status](https://david-dm.org/sombrerohq/sombrero-node.svg)](https://david-dm.org/sombrerohq/sombrero-node) 5 | 6 | Node of a Sombrero cluster. Uses [Skiff](https://github.com/pgte/skiff) (A [Raft](http://raftconsensus.github.io/) implementation) underneath. 7 | 8 | * As a leader, creates an independent RPC server for servicing requests from followers. 9 | * As a follower, forwards the write requests to the leader. 10 | * As a follower, makes sure that the leader writes to the issuing follower before returning. 11 | * Because of this last point, it implements a read-your-writes on a follower. 12 | 13 | ## Install 14 | 15 | ```bash 16 | $ npm install sombrero-node --save 17 | ``` 18 | 19 | ## Require 20 | 21 | ```javascript 22 | var Node = require('sombrero-node'); 23 | ``` 24 | 25 | ## Create 26 | 27 | ```javascript 28 | var url = 'tcp+msgpack://localhost:8000'; 29 | var port = 7000; 30 | var options = { 31 | skiff: { 32 | dbPath: '/path/to/my/leveldb/dir' 33 | }, 34 | port: port 35 | }; 36 | 37 | var node = Node(url, options); 38 | ``` 39 | 40 | ### Options 41 | 42 | * `skiff`: all the [options supported by skiff](https://github.com/pgte/skiff#options) 43 | * `port`: the TCP port for exposing the RPC server. Defaults to `5201`. 44 | * `transport`: a transport module provider. Defaults to [this](https://github.com/sombrerohq/sombrero-node/blob/master/lib/transport.js). 45 | * `gossip`: an object with the following attributes: 46 | * `port`: gossip port. defaults to 8217 47 | 48 | # Use 49 | 50 | A Sombrero node implements [the level-up API](https://github.com/rvagg/node-levelup#api). 51 | 52 | You can also extend the client with level-* plugins, including [sublevel](https://github.com/dominictarr/level-sublevel). 53 | 54 | ## .join(url, options, cb) 55 | 56 | Joins a node given its URL. Options should contain the hostname and port of the node Sombrero RPC server. 57 | 58 | Example: 59 | 60 | ```javascript 61 | var options = { 62 | hostname: 'localhost', 63 | port: 8071 64 | }; 65 | 66 | node.join('tcp+msgpack://hostname:8000', options, function(err) { 67 | if (err) throw err; 68 | console.log('joined'); 69 | }); 70 | ``` 71 | 72 | ## .leave(url, cb) 73 | 74 | Leaves a node given its URL. 75 | 76 | ## .close(cb) 77 | 78 | Closes the server and the skiff node. 79 | 80 | ## Events 81 | 82 | A Sombrero node emits the same [events as a Skiff node](https://github.com/pgte/skiff#events). 83 | 84 | 85 | # Setting up a cluster 86 | 87 | To boot a cluster, start a node and wait for it to become a leader. Then, create each additional node in the `standby` mode (`options.skiff.standby: true`) and do `leader.join(nodeURL)`. 88 | 89 | See [this test](https://github.com/sombrerohq/sombrero-node/blob/master/tests/networked.js) for an actual implementation. 90 | 91 | # License 92 | 93 | ISC -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | # Vagrantfile API/syntax version. Don't touch unless you know what you're doing! 5 | VAGRANTFILE_API_VERSION = "2" 6 | 7 | Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| 8 | # All Vagrant configuration is done here. The most common configuration 9 | # options are documented and commented below. For a complete reference, 10 | # please see the online documentation at vagrantup.com. 11 | 12 | # Every Vagrant virtual environment requires a box to build off of. 13 | config.vm.box = "hashicorp/precise64" 14 | 15 | # Disable automatic box update checking. If you disable this, then 16 | # boxes will only be checked for updates when the user runs 17 | # `vagrant box outdated`. This is not recommended. 18 | # config.vm.box_check_update = false 19 | 20 | # Create a forwarded port mapping which allows access to a specific port 21 | # within the machine from a port on the host machine. In the example below, 22 | # accessing "localhost:8080" will access port 80 on the guest machine. 23 | # config.vm.network "forwarded_port", guest: 80, host: 8080 24 | 25 | # Create a private network, which allows host-only access to the machine 26 | # using a specific IP. 27 | # config.vm.network "private_network", ip: "192.168.33.10" 28 | 29 | # Create a public network, which generally matched to bridged network. 30 | # Bridged networks make the machine appear as another physical device on 31 | # your network. 32 | # config.vm.network "public_network" 33 | 34 | # If true, then any SSH connections made will enable agent forwarding. 35 | # Default value: false 36 | # config.ssh.forward_agent = true 37 | 38 | # Share an additional folder to the guest VM. The first argument is 39 | # the path on the host to the actual folder. The second argument is 40 | # the path on the guest to mount the folder. And the optional third 41 | # argument is a set of non-required options. 42 | # config.vm.synced_folder "../data", "/vagrant_data" 43 | 44 | # Provider-specific configuration so you can fine-tune various 45 | # backing providers for Vagrant. These expose provider-specific options. 46 | # Example for VirtualBox: 47 | # 48 | config.vm.provider "virtualbox" do |vb| 49 | # Don't boot with headless mode 50 | # vb.gui = true 51 | # 52 | # Use VBoxManage to customize the VM. For example to change memory: 53 | vb.customize ["modifyvm", :id, "--memory", "1024"] 54 | end 55 | 56 | # View the documentation for the provider you're using for more 57 | # information on available options. 58 | 59 | # Enable provisioning with CFEngine. CFEngine Community packages are 60 | # automatically installed. For example, configure the host as a 61 | # policy server and optionally a policy file to run: 62 | # 63 | # config.vm.provision "cfengine" do |cf| 64 | # cf.am_policy_hub = true 65 | # # cf.run_file = "motd.cf" 66 | # end 67 | # 68 | # You can also configure and bootstrap a client to an existing 69 | # policy server: 70 | # 71 | # config.vm.provision "cfengine" do |cf| 72 | # cf.policy_server_address = "10.0.2.15" 73 | # end 74 | 75 | # Enable provisioning with Puppet stand alone. Puppet manifests 76 | # are contained in a directory path relative to this Vagrantfile. 77 | # You will need to create the manifests directory and a manifest in 78 | # the file default.pp in the manifests_path directory. 79 | # 80 | # config.vm.provision "puppet" do |puppet| 81 | # puppet.manifests_path = "manifests" 82 | # puppet.manifest_file = "default.pp" 83 | # end 84 | 85 | # Enable provisioning with chef solo, specifying a cookbooks path, roles 86 | # path, and data_bags path (all relative to this Vagrantfile), and adding 87 | # some recipes and/or roles. 88 | # 89 | # config.vm.provision "chef_solo" do |chef| 90 | # chef.cookbooks_path = "../my-recipes/cookbooks" 91 | # chef.roles_path = "../my-recipes/roles" 92 | # chef.data_bags_path = "../my-recipes/data_bags" 93 | # chef.add_recipe "mysql" 94 | # chef.add_role "web" 95 | # 96 | # # You may also specify custom JSON attributes: 97 | # chef.json = { mysql_password: "foo" } 98 | # end 99 | 100 | # Enable provisioning with chef server, specifying the chef server URL, 101 | # and the path to the validation key (relative to this Vagrantfile). 102 | # 103 | # The Opscode Platform uses HTTPS. Substitute your organization for 104 | # ORGNAME in the URL and validation key. 105 | # 106 | # If you have your own Chef Server, use the appropriate URL, which may be 107 | # HTTP instead of HTTPS depending on your configuration. Also change the 108 | # validation key to validation.pem. 109 | # 110 | # config.vm.provision "chef_client" do |chef| 111 | # chef.chef_server_url = "https://api.opscode.com/organizations/ORGNAME" 112 | # chef.validation_key_path = "ORGNAME-validator.pem" 113 | # end 114 | # 115 | # If you're using the Opscode platform, your validator client is 116 | # ORGNAME-validator, replacing ORGNAME with your organization name. 117 | # 118 | # If you have your own Chef Server, the default validation client name is 119 | # chef-validator, unless you changed the configuration. 120 | # 121 | # chef.validation_client_name = "ORGNAME-validator" 122 | end 123 | -------------------------------------------------------------------------------- /lib/default_options.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | skiff: {}, 3 | transport: require('./transport'), 4 | port: 5201, 5 | gossip: { 6 | port: 8217 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /lib/node.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var URL = require('url'); 4 | var Peer = require('./peer'); 5 | var Skiff = require('skiff'); 6 | var Gossip = require('sombrero-gossip'); 7 | var Server = require('./server'); 8 | var extend = require('xtend'); 9 | var inherits = require('util').inherits; 10 | var propagate = require('propagate'); 11 | var EventEmitter = require('events').EventEmitter; 12 | var defaultOptions = require('./default_options'); 13 | var BatchWriteStream = require('batch-write-stream'); 14 | 15 | module.exports = Node; 16 | 17 | function Node(skiffURL, options) { 18 | if (!(this instanceof Node)) { 19 | return new Node(skiffURL, options); 20 | } 21 | 22 | EventEmitter.call(this); 23 | 24 | var self = this; 25 | 26 | options = extend({}, defaultOptions, options); 27 | 28 | var url = URL.parse(skiffURL); 29 | this.metadata = { 30 | gossipPort: options.gossip.port, 31 | hostname: url.hostname, 32 | port: options.port 33 | }; 34 | options.skiff.metadata = this.metadata; 35 | 36 | this._options = options; 37 | 38 | if (!skiffURL) { 39 | throw new Error('need skiff URL'); 40 | } 41 | this.id = skiffURL; 42 | 43 | this.skiff = Skiff(skiffURL, options.skiff); 44 | 45 | this.skiff.on('joined', onPeerJoined); 46 | this.skiff.on('reconnected', onPeerJoined); 47 | function onPeerJoined(peer) { 48 | if (peer && peer.id != self.id && 49 | peer.metadata && peer.metadata.gossipPort) { 50 | self.gossip.addPeer({ 51 | id: peer.id, 52 | hostname: peer.metadata.hostname, 53 | port: peer.metadata.gossipPort 54 | }); 55 | } 56 | } 57 | 58 | this.skiff.on('leader', onLeader); 59 | function onLeader() { 60 | self._gossipLeader(self.id); 61 | } 62 | 63 | this.skiff.on('left', function(peer) { 64 | self.gossip.removePeer(peer.id); 65 | }); 66 | 67 | propagate(this.skiff, this); 68 | 69 | this._remotes = {}; 70 | 71 | this._levelServer = Server(this.skiff); 72 | this._transportServer = options.transport.listen( 73 | options.port, options.hostname, this._levelServer, listening); 74 | 75 | this.gossip = Gossip(this._options.gossip); 76 | this.gossip.on('error', function(err) { 77 | self.emit('error', err); 78 | }); 79 | this.gossip.on('cluster change', function(change) { 80 | self.emit('cluster change', change); 81 | }); 82 | 83 | function listening(err) { 84 | if (err) { 85 | self.emit('error', err); 86 | } 87 | } 88 | } 89 | 90 | inherits(Node, EventEmitter); 91 | 92 | var N = Node.prototype; 93 | 94 | N.put = function put(key, value, options, callback) { 95 | if (typeof options == 'function') { 96 | callback = options; 97 | options = {}; 98 | } 99 | options = this._waitForNode(options); 100 | this._call('put', key, value, options, callback); 101 | }; 102 | 103 | N.get = function get(key, options, callback) { 104 | this._call('get', key, options, callback); 105 | }; 106 | 107 | N.del = function del(key, options, callback) { 108 | if (typeof options == 'function') { 109 | callback = options; 110 | options = {}; 111 | } 112 | options = this._waitForNode(options); 113 | this._call('del', key, options, callback); 114 | }; 115 | 116 | N.batch = function batch(b, options, callback) { 117 | if (typeof options == 'function') { 118 | callback = options; 119 | options = {}; 120 | } 121 | options = this._waitForNode(options); 122 | this._call('batch', b, options, callback); 123 | }; 124 | 125 | N.createWriteStream = function createWriteStream(options) { 126 | var self = this; 127 | var ws = new BatchWriteStream(options); 128 | ws._writeBatch = function _writeBatch(batch, cb) { 129 | self.batch(batch, cb); 130 | }; 131 | 132 | return ws; 133 | }; 134 | 135 | N.createReadStream = function createReadStream(options) { 136 | return this.skiff.createReadStream(options); 137 | }; 138 | 139 | N.iterator = function iterator(options) { 140 | return this.skiff.iterator(options); 141 | }; 142 | 143 | N.join = function join(url, options, cb) { 144 | this.skiff.join(url, options || null, cb); 145 | }; 146 | 147 | N.leave = function leave(url, cb) { 148 | this.skiff.leave(url, cb); 149 | }; 150 | 151 | N.open = function open(cb) { 152 | this.skiff.open(cb); 153 | }; 154 | 155 | N.close = function close(cb) { 156 | var self = this; 157 | 158 | this._transportServer.close(function() { 159 | self.gossip.stop(function() { 160 | self.skiff.close(cb); 161 | }); 162 | }); 163 | }; 164 | 165 | N._call = function _call(methodName) { 166 | var self = this; 167 | 168 | var method = this.skiff[methodName]; 169 | var args = Array.prototype.slice.call(arguments); 170 | args.shift(); 171 | 172 | for (var i = args.length - 1 ; i >= 0 ; i --) { 173 | if (args[i] === undefined) { 174 | args.pop(); 175 | } else { 176 | break; 177 | } 178 | } 179 | 180 | var cb = args[args.length - 1]; 181 | if (typeof cb != 'function') { 182 | cb = undefined; 183 | } 184 | if (cb) { 185 | args.pop(); 186 | } 187 | 188 | args.push(replied); 189 | method.apply(this.skiff, args); 190 | 191 | function replied(err) { 192 | if (!err || !err.leader) { 193 | if (cb) { 194 | cb.apply(null, arguments); 195 | } else { 196 | self.emit('error', err); 197 | } 198 | } else { 199 | var remoteArgs = args.slice(0, args.length - 1); 200 | self._remoteCall.call(self, err.leader, methodName, remoteArgs, replied); 201 | self._gossipLeader(err.leader); 202 | } 203 | } 204 | }; 205 | 206 | N._remoteCall = function _remoteCall(node, method, args, cb) { 207 | var remote = this._remote(node); 208 | if (!remote) { 209 | cb(new Error('could not find remote metadata for URL ' + node)); 210 | } 211 | else if (remote.connected) { 212 | invoke(remote.client); 213 | } else { 214 | remote.once('connect', invoke); 215 | } 216 | 217 | function invoke(client) { 218 | var m = client[method]; 219 | if (!m) { 220 | throw new Error('Method not found: ' + method); 221 | } 222 | args.push(cb); 223 | m.apply(client, args); 224 | } 225 | }; 226 | 227 | N._remote = function _remote(node) { 228 | var remote = this._remotes[node]; 229 | if (!remote) { 230 | var meta = this.skiff.peerMeta(node); 231 | if (meta) { 232 | remote = this._remotes[node] = Peer(meta.hostname, meta.port); 233 | } 234 | } 235 | return remote; 236 | }; 237 | 238 | N._waitForNode = function _waitForNode(options) { 239 | if (!options) { 240 | options = {}; 241 | } 242 | options.waitForNode = this.id; 243 | return options; 244 | }; 245 | 246 | N._gossipLeader = function gossipLeader(leader) { 247 | var currentLeader = this.gossip.cluster.get('leader'); 248 | if (currentLeader != leader) { 249 | this.gossip.cluster.set('leader', leader); 250 | } 251 | }; 252 | -------------------------------------------------------------------------------- /lib/peer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var transport = require('./transport'); 4 | 5 | module.exports = Peer; 6 | 7 | function Peer(hostname, port) { 8 | return transport.connect(hostname, port); 9 | } 10 | -------------------------------------------------------------------------------- /lib/server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = Server; 4 | 5 | function Server(skiff) { 6 | if (!(this instanceof Server)) { 7 | return new Server(skiff); 8 | } 9 | 10 | this.skiff = skiff; 11 | } 12 | 13 | var S = Server.prototype; 14 | 15 | S.put = function put(key, value, options, cb) { 16 | this.skiff.put(key, value, options, cb); 17 | }; 18 | 19 | S.del = function del(key, options, cb) { 20 | this.skiff.del(key, options, cb); 21 | }; 22 | 23 | S.batch = function del(key, options, cb) { 24 | this.skiff.batch(key, options, cb); 25 | }; 26 | 27 | Server.methods = Object.keys(S); 28 | -------------------------------------------------------------------------------- /lib/transport.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var net = require('net'); 4 | var rpc = require('rpc-stream'); 5 | var once = require('once'); 6 | var Server = require('./server'); 7 | var MsgPack = require('msgpack-stream'); 8 | var reconnect = require('reconnect-net'); 9 | var EventEmitter = require('events').EventEmitter; 10 | 11 | exports.connect = connect; 12 | 13 | function connect(hostname, port) { 14 | var r = reconnect().connect(port, hostname); 15 | var ee = new EventEmitter(); 16 | ee.connected = false; 17 | 18 | ee.disconnect = disconnect; 19 | 20 | r.on('connect', onConnect); 21 | r.on('reconnect', onReconnect); 22 | r.on('disconnect', onDisconnect); 23 | 24 | return ee; 25 | 26 | function onConnect(con) { 27 | var remote = rpc(); 28 | remote.pipe(MsgPack.createEncodeStream()).pipe(con); 29 | con.pipe(MsgPack.createDecodeStream()).pipe(remote); 30 | 31 | var client = remote.wrap(Server.methods); 32 | 33 | ee.connected = true; 34 | ee.client = client; 35 | ee.emit('connect', client); 36 | } 37 | 38 | function onReconnect() { 39 | ee.emit('reconnect'); 40 | } 41 | 42 | function onDisconnect() { 43 | ee.connected = false; 44 | ee.emit('disconnect'); 45 | } 46 | 47 | function disconnect() { 48 | r.disconnect(); 49 | } 50 | } 51 | 52 | exports.listen = listen; 53 | 54 | function listen(port, hostname, server, callback) { 55 | var netServer = net.createServer(onConnection); 56 | netServer.listen(port, hostname, onceListening); 57 | callback = once(callback); 58 | netServer.once('error', callback); 59 | 60 | netServer.__connections = []; 61 | 62 | var serverClose = netServer.close; 63 | netServer.close = function close(cb) { 64 | serverClose.call(netServer, cb); 65 | netServer.__connections.forEach(function(con) { 66 | con.end(); 67 | }); 68 | }; 69 | 70 | return netServer; 71 | 72 | function onceListening() { 73 | netServer.removeListener('error', callback); 74 | callback(); 75 | } 76 | 77 | function onConnection(con) { 78 | netServer.__connections.push(con); 79 | var service = rpc(server); 80 | con.pipe(MsgPack.createDecodeStream()).pipe(service). 81 | pipe(MsgPack.createEncodeStream()).pipe(con); 82 | 83 | con.once('close', function() { 84 | var idx = netServer.__connections.indexOf(con); 85 | if (idx >= 0) { 86 | netServer.__connections.splice(idx, 1); 87 | } 88 | }); 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sombrero-node", 3 | "version": "0.8.8", 4 | "description": "Sombrero Cluster Node", 5 | "scripts": { 6 | "test": "istanbul cover lab -- -v tests/*.js -l && istanbul check-coverage --statements 60 --functions 60 -- lines 65 --branches 60", 7 | "jshint": "jshint -c .jshintrc --exclude-path .gitignore .", 8 | "codestyle": "jscs -p google lib/ tests/", 9 | "coverage": "open coverage/lcov-report/index.html" 10 | }, 11 | "main": "lib/node.js", 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/pgte/skiff.git" 15 | }, 16 | "keywords": [ 17 | "raft", 18 | "distributed", 19 | "consensus", 20 | "election", 21 | "vote" 22 | ], 23 | "author": "pgte", 24 | "license": "ISC", 25 | "bugs": { 26 | "url": "https://github.com/pgte/skiff/issues" 27 | }, 28 | "homepage": "https://github.com/pgte/skiff", 29 | "devDependencies": { 30 | "concat-stream": "^1.4.6", 31 | "istanbul": "^0.3.2", 32 | "jscs": "^1.6.2", 33 | "jshint": "^2.5.6", 34 | "lab": "^4.5.1", 35 | "level-sublevel": "^6.3.15", 36 | "mkdirp": "^0.5.0", 37 | "pre-commit": "0.0.9", 38 | "rimraf": "^2.2.8" 39 | }, 40 | "dependencies": { 41 | "async": "^0.9.0", 42 | "batch-write-stream": "^0.1.6", 43 | "msgpack-stream": "0.0.12", 44 | "mux-demux": "^3.7.9", 45 | "once": "^1.3.1", 46 | "propagate": "^0.3.0", 47 | "reconnect-net": "0.0.0", 48 | "rpc-stream": "^2.1.1", 49 | "skiff": "^0.8.5", 50 | "sombrero-gossip": "^0.8.1", 51 | "xtend": "^4.0.0" 52 | }, 53 | "pre-commit": [ 54 | "codestyle", 55 | "jshint", 56 | "test" 57 | ] 58 | } 59 | -------------------------------------------------------------------------------- /tests/_debug.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function Logger(node) { 4 | return function log() { 5 | var s = arguments[0] || ''; 6 | s = '[' + Date.now() + '] [' + node.id + '] ' + s; 7 | arguments[0] = s; 8 | console.log.apply(console, arguments); 9 | }; 10 | } 11 | 12 | module.exports = debug; 13 | 14 | function debug(node) { 15 | var log = Logger(node); 16 | node.once('loaded', function() { 17 | log('loaded'); 18 | }); 19 | node.on('state', function(state) { 20 | log('state:', state); 21 | }); 22 | node.on('AppendEntries', function(args) { 23 | log('-> AppendEntries: %j', args); 24 | }); 25 | node.on('RequestVote', function(args) { 26 | log('-> RequestVote: %j', args); 27 | }); 28 | node.on('vote granted', function(node) { 29 | log('vote granted to', node); 30 | }); 31 | node.on('outgoing call', function(peer, type, message) { 32 | log('<- outgoing call to %s of type "%s": %j', peer.id, type, message); 33 | }); 34 | node.on('response', function(peer, err, args) { 35 | log('<- response: %j', peer.id, err, args); 36 | }); 37 | node.on('election timeout', function() { 38 | log('election timeout'); 39 | }); 40 | node.on('reply', function() { 41 | log('-> reply %j', arguments); 42 | }); 43 | node.on('reset election timeout', function() { 44 | log('reset election timeout'); 45 | }); 46 | node.on('joined', function(peer) { 47 | log('joined %s', peer.id); 48 | }); 49 | node.on('connecting', function(peer) { 50 | log('connecting to %s', peer.id); 51 | }); 52 | node.on('connected', function(peer) { 53 | log('connected from %s', peer.id); 54 | }); 55 | node.on('disconnected', function(peer) { 56 | log('disconnected from %s', peer.id); 57 | }); 58 | node.on('listening', function(address) { 59 | log('listening on %s', address); 60 | }); 61 | node.on('cluster change', function(change) { 62 | log('gossiped cluster change:', change); 63 | }); 64 | } 65 | 66 | debug.debug2 = function(node) { 67 | var log = Logger(node); 68 | node.on('state', function(state) { 69 | log(state, node.currentTerm()); 70 | }); 71 | node.on('vote granted', function(node) { 72 | log('voted for', node); 73 | }); 74 | node.on('AppendEntries', function(args) { 75 | log('AppendEntries from', args[0].leaderId); 76 | }); 77 | node.on('election timeout', function() { 78 | log('timed out'); 79 | }); 80 | }; 81 | -------------------------------------------------------------------------------- /tests/compatibility.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Lab = require('lab'); 4 | var lab = exports.lab = Lab.script(); 5 | var describe = lab.describe; 6 | var it = lab.it; 7 | var assert = Lab.assert; 8 | 9 | var sublevel = require('level-sublevel'); 10 | 11 | var path = require('path'); 12 | var async = require('async'); 13 | var concat = require('concat-stream'); 14 | var rimraf = require('rimraf'); 15 | var mkdirp = require('mkdirp'); 16 | 17 | var Client = require('../'); 18 | 19 | describe('standalone client', function() { 20 | var client; 21 | var dbPath = path.join(__dirname, '..', 'db', 'compatibility'); 22 | 23 | rimraf.sync(dbPath); 24 | mkdirp.sync(dbPath); 25 | 26 | it('supports sublevel', function(done) { 27 | client = Client('tcp+msgpack://localhost:8060', { 28 | skiff: {dbPath: dbPath}, 29 | port: 7010 30 | }); 31 | client = sublevel(client); 32 | client = client.sublevel('level1'); 33 | done(); 34 | }); 35 | 36 | it('allows putting', function(done) { 37 | client.put('key', 'value', done); 38 | }); 39 | 40 | it('allows getting', function(done) { 41 | client.get('key', function(err, value) { 42 | if (err) { 43 | throw err; 44 | } 45 | assert.equal(value, 'value'); 46 | done(); 47 | }); 48 | }); 49 | 50 | it('allows batching', function(done) { 51 | client.batch([ 52 | {type: 'put', key: 'key a', value: 'value a'}, 53 | {type: 'put', key: 'key b', value: 'value b'}, 54 | {type: 'put', key: 'key c', value: 'value c'}, 55 | {type: 'put', key: 'key c', value: 'value c2'}, 56 | {type: 'del', key: 'key a'} 57 | ], done); 58 | }); 59 | 60 | it('batch worked', function(done) { 61 | async.map(['key b', 'key c'], client.get.bind(client), resulted); 62 | 63 | function resulted(err, results) { 64 | if (err) { 65 | throw err; 66 | } 67 | assert.deepEqual(results, ['value b', 'value c2']); 68 | 69 | client.get('key a', function(err) { 70 | assert(err && err.notFound); 71 | done(); 72 | }); 73 | } 74 | }); 75 | 76 | it('can create a read stream with no args', function(done) { 77 | client.createReadStream().pipe(concat(function(values) { 78 | assert.deepEqual(values, [ 79 | {key: 'key', value: 'value'}, 80 | {key: 'key b', value: 'value b'}, 81 | {key: 'key c', value: 'value c2'} 82 | ]); 83 | done(); 84 | })); 85 | }); 86 | 87 | it('can create a read stream with some args', function(done) { 88 | client.createReadStream({ 89 | gte: 'key b', 90 | lte: 'key c' 91 | }).pipe(concat(function(values) { 92 | assert.deepEqual(values, [ 93 | {key: 'key b', value: 'value b'}, 94 | {key: 'key c', value: 'value c2'} 95 | ]); 96 | done(); 97 | })); 98 | }); 99 | 100 | it('closes', function(done) { 101 | client.close(done); 102 | }); 103 | }); 104 | -------------------------------------------------------------------------------- /tests/gossip.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Lab = require('lab'); 4 | var lab = exports.lab = Lab.script(); 5 | var it = lab.it; 6 | var assert = Lab.assert; 7 | var describe = lab.describe; 8 | 9 | var path = require('path'); 10 | var async = require('async'); 11 | var rimraf = require('rimraf'); 12 | var mkdirp = require('mkdirp'); 13 | 14 | var Node = require('../'); 15 | 16 | describe('gossip', function() { 17 | 18 | var dbPath = path.join(__dirname, '..', 'db', 'gossip'); 19 | 20 | rimraf.sync(dbPath); 21 | mkdirp.sync(dbPath); 22 | 23 | var leader; 24 | var newLeader; 25 | var lastFollower; 26 | var nodes = []; 27 | 28 | it('can create leader', function(done) { 29 | leader = Node('tcp+msgpack://localhost:8090', { 30 | skiff: {dbPath: path.join(dbPath, 'leader')}, 31 | port: 8030, 32 | gossip: { 33 | port: 7020 34 | } 35 | }); 36 | nodes.push(leader); 37 | leader.once('leader', function() { 38 | done(); 39 | }); 40 | }); 41 | 42 | it('create 4 followers', function(done) { 43 | var node; 44 | for (var i = 0 ; i < 4 ; i ++) { 45 | node = Node('tcp+msgpack://localhost:' + (8091 + i), { 46 | skiff: { 47 | dbPath: path.join(dbPath, 'node' + i), 48 | standby: true 49 | }, 50 | port: 8031 + i, 51 | gossip: { 52 | port: 7031 + i 53 | } 54 | }); 55 | nodes.push(node); 56 | } 57 | 58 | done(); 59 | }); 60 | 61 | it('can join followers', function(done) { 62 | async.each(nodes, function(node, cb) { 63 | if (node.id != leader.id) { 64 | leader.join(node.id, node.skiff.metadata, cb); 65 | } 66 | else { 67 | cb(); 68 | } 69 | }, done); 70 | }); 71 | 72 | it('waits a bit', function(done) { 73 | setTimeout(done, 1e3); 74 | }); 75 | 76 | it('follower puts', function(done) { 77 | nodes[1].put('key', 'value', done); 78 | }); 79 | 80 | it('waits a bit', {timeout: 4e3}, function(done) { 81 | setTimeout(done, 3e3); 82 | }); 83 | 84 | it('every node knows the leader', function(done) { 85 | nodes.forEach(function(node) { 86 | assert.equal(node.gossip.cluster.get('leader'), leader.id); 87 | }); 88 | done(); 89 | }); 90 | 91 | it('leader dies', {timeout: 6e3}, function(done) { 92 | nodes.forEach(function(node) { 93 | node.once('leader', haveLeader); 94 | }); 95 | 96 | function haveLeader(node) { 97 | if (!newLeader) { 98 | newLeader = node; 99 | // debug(newLeader); 100 | done(); 101 | } 102 | } 103 | 104 | leader.close(function(err) { 105 | if (err) { 106 | throw err; 107 | } 108 | }); 109 | }); 110 | 111 | it('has a follower', function(done) { 112 | var node; 113 | for (var i = 1 ; i < nodes.length ; i ++) { 114 | node = nodes[i]; 115 | if (node.id != newLeader.id && node.id != leader.id) { 116 | lastFollower = node; 117 | break; 118 | } 119 | } 120 | assert(!!lastFollower, 'has last follower'); 121 | assert.notEqual(lastFollower.id, newLeader.id); 122 | done(); 123 | }); 124 | 125 | it('waits a bit', {timeout: 15e3}, function(done) { 126 | setTimeout(done, 14e3); 127 | }); 128 | 129 | it('every node but the old leader knows the leader', function(done) { 130 | nodes.forEach(function(node) { 131 | if (node != leader) { 132 | assert.equal(node.gossip.cluster.get('leader'), newLeader.id, 133 | node.id + ' thinks the leader is ' + 134 | node.gossip.cluster.get('leader')); 135 | } 136 | }); 137 | done(); 138 | }); 139 | 140 | it('closes all nodes', {timeout: 10e3}, function(done) { 141 | async.each(nodes, function(node, cb) { 142 | if (node != leader) { 143 | node.close(cb); 144 | } 145 | else { 146 | cb(); 147 | } 148 | }, done); 149 | }); 150 | 151 | }); 152 | -------------------------------------------------------------------------------- /tests/networked.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Lab = require('lab'); 4 | var lab = exports.lab = Lab.script(); 5 | var it = lab.it; 6 | var assert = Lab.assert; 7 | var describe = lab.describe; 8 | 9 | var path = require('path'); 10 | var async = require('async'); 11 | var concat = require('concat-stream'); 12 | var rimraf = require('rimraf'); 13 | var mkdirp = require('mkdirp'); 14 | 15 | var Node = require('../'); 16 | 17 | describe('networked', function() { 18 | 19 | var dbPath = path.join(__dirname, '..', 'db', 'networked'); 20 | 21 | rimraf.sync(dbPath); 22 | mkdirp.sync(dbPath); 23 | 24 | var leader; 25 | var follower; 26 | var commands; 27 | 28 | it('can create leader', function(done) { 29 | leader = Node('tcp+msgpack://localhost:8090', { 30 | skiff: {dbPath: path.join(dbPath, 'leader')}, 31 | port: 8070, 32 | gossip: { 33 | port: 7070 34 | } 35 | }); 36 | leader.once('leader', function() { 37 | done(); 38 | }); 39 | }); 40 | 41 | it('create follower', function(done) { 42 | follower = Node('tcp+msgpack://localhost:8091', { 43 | skiff: { 44 | dbPath: path.join(dbPath, 'follower'), 45 | standby: true 46 | }, 47 | port: 8071, 48 | gossip: { 49 | port: 7071 50 | } 51 | }); 52 | done(); 53 | }); 54 | 55 | it('can join follower', function(done) { 56 | leader.join(follower.id, { 57 | hostname: 'localhost', 58 | port: 8071, 59 | gossipPort: 7071 60 | }, done); 61 | }); 62 | 63 | it('waits a bit', function(done) { 64 | setTimeout(done, 1e3); 65 | }); 66 | 67 | it('can put', function(done) { 68 | follower.put('key', 'value', done); 69 | }); 70 | 71 | it('can get', function(done) { 72 | follower.get('key', function(err, value) { 73 | if (err) { 74 | done(err); 75 | } 76 | assert.equal(value, 'value'); 77 | done(); 78 | }); 79 | }); 80 | 81 | it('handles errors on callback', function(done) { 82 | follower.get('doesnotexist', function(err) { 83 | assert.instanceOf(err, Error); 84 | assert.equal(err.message, 'Key not found in database'); 85 | done(); 86 | }); 87 | }); 88 | 89 | it('handles errors by emitting error when no callback', function(done) { 90 | follower.get('doesnotexist'); 91 | follower.once('error', function(err) { 92 | assert.instanceOf(err, Error); 93 | assert.equal(err.message, 'Key not found in database'); 94 | done(); 95 | }); 96 | }); 97 | 98 | it('can delete', function(done) { 99 | follower.del('key', done); 100 | }); 101 | 102 | it('delete worked', function(done) { 103 | follower.get('key', function(err) { 104 | assert.instanceOf(err, Error); 105 | assert.equal(err.message, 'Key not found in database'); 106 | done(); 107 | }); 108 | }); 109 | 110 | it('can create a write stream', function(done) { 111 | commands = []; 112 | for (var i = 0 ; i < 10 ; i ++) { 113 | commands.push({ 114 | type: 'put', 115 | key: 'key ' + i, 116 | value: 'value ' + i 117 | }); 118 | } 119 | var ws = follower.createWriteStream(); 120 | async.each(commands, ws.write.bind(ws), done); 121 | }); 122 | 123 | it('can create a read stream', function(done) { 124 | commands.forEach(function(command) { 125 | delete command.type; 126 | }); 127 | 128 | follower.createReadStream().pipe(concat(function(data) { 129 | assert.deepEqual(data, commands); 130 | })).once('finish', done); 131 | }); 132 | 133 | it('can batch', function(done) { 134 | follower.batch([ 135 | { 136 | type: 'put', 137 | key: 'a', 138 | value: 'A' 139 | }, 140 | { 141 | type: 'put', 142 | key: 'b', 143 | value: 'B' 144 | } 145 | ], done); 146 | }); 147 | 148 | it('batch worked', function(done) { 149 | async.map(['a', 'b'], follower.get.bind(follower), function(err, values) { 150 | if (err) { 151 | throw err; 152 | } 153 | assert.deepEqual(values, ['A', 'B']); 154 | done(); 155 | }); 156 | }); 157 | 158 | it('closes all nodes', function(done) { 159 | async.each([leader, follower], function(node, cb) { 160 | node.close(cb); 161 | }, done); 162 | }); 163 | 164 | }); 165 | -------------------------------------------------------------------------------- /tests/standalone.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Lab = require('lab'); 4 | var lab = exports.lab = Lab.script(); 5 | var it = lab.it; 6 | var assert = Lab.assert; 7 | var describe = lab.describe; 8 | 9 | var path = require('path'); 10 | var async = require('async'); 11 | var concat = require('concat-stream'); 12 | var rimraf = require('rimraf'); 13 | var mkdirp = require('mkdirp'); 14 | 15 | var Node = require('../'); 16 | 17 | describe('standalone', function() { 18 | 19 | var dbPath = path.join(__dirname, '..', 'db', 'standalone'); 20 | 21 | rimraf.sync(dbPath); 22 | mkdirp.sync(dbPath); 23 | 24 | var node; 25 | var commands; 26 | 27 | it('can get created', function(done) { 28 | node = Node('tcp+msgpack://localhost:8080', { 29 | skiff: {dbPath: dbPath}, 30 | port: 7000, 31 | gossip: { 32 | port: 6000 33 | } 34 | }); 35 | done(); 36 | }); 37 | 38 | it('can put', function(done) { 39 | node.put('key', 'value', done); 40 | }); 41 | 42 | it('can get', function(done) { 43 | node.get('key', function(err, value) { 44 | if (err) { 45 | throw err; 46 | } 47 | assert.equal(value, 'value'); 48 | done(); 49 | }); 50 | }); 51 | 52 | it('handles errors on callback', function(done) { 53 | node.get('doesnotexist', function(err) { 54 | assert.instanceOf(err, Error); 55 | assert.equal(err.message, 'Key not found in database'); 56 | done(); 57 | }); 58 | }); 59 | 60 | it('handles errors by emitting error when no callback', function(done) { 61 | node.get('doesnotexist'); 62 | node.once('error', function(err) { 63 | assert.instanceOf(err, Error); 64 | assert.equal(err.message, 'Key not found in database'); 65 | done(); 66 | }); 67 | }); 68 | 69 | it('can delete', function(done) { 70 | node.del('key', done); 71 | }); 72 | 73 | it('delete worked', function(done) { 74 | node.get('key', function(err) { 75 | assert.instanceOf(err, Error); 76 | assert.equal(err.message, 'Key not found in database'); 77 | done(); 78 | }); 79 | }); 80 | 81 | it('can create a write stream', function(done) { 82 | commands = []; 83 | for (var i = 0 ; i < 10 ; i ++) { 84 | commands.push({ 85 | type: 'put', 86 | key: 'key ' + i, 87 | value: 'value ' + i 88 | }); 89 | } 90 | var ws = node.createWriteStream(); 91 | async.each(commands, ws.write.bind(ws), done); 92 | }); 93 | 94 | it('can create a read stream', function(done) { 95 | commands.forEach(function(command) { 96 | delete command.type; 97 | }); 98 | 99 | node.createReadStream().pipe(concat(function(data) { 100 | assert.deepEqual(data, commands); 101 | })).once('finish', done); 102 | }); 103 | 104 | it('can batch', function(done) { 105 | node.batch([ 106 | { 107 | type: 'put', 108 | key: 'a', 109 | value: 'A' 110 | }, 111 | { 112 | type: 'put', 113 | key: 'b', 114 | value: 'B' 115 | } 116 | ], done); 117 | }); 118 | 119 | it('batch worked', function(done) { 120 | async.map(['a', 'b'], node.get.bind(node), function(err, values) { 121 | if (err) { 122 | throw err; 123 | } 124 | assert.deepEqual(values, ['A', 'B']); 125 | done(); 126 | }); 127 | }); 128 | 129 | it('closes', function(done) { 130 | node.close(done); 131 | }); 132 | }); 133 | --------------------------------------------------------------------------------