Repository: mrhooray/swim-js Branch: master Commit: bd3d2f87d60e Files: 30 Total size: 116.0 KB Directory structure: gitextract_5lnnmlc_/ ├── .gitignore ├── .jscsrc ├── .jshintrc ├── .npmignore ├── .travis.yml ├── README.md ├── bench/ │ ├── index.js │ ├── lib/ │ │ └── runner.js │ ├── scenario/ │ │ └── single-node-failure.js │ └── script/ │ ├── convergence-time.js │ ├── dumb.js │ └── worker.js ├── docs/ │ └── api.md ├── index.js ├── lib/ │ ├── codec.js │ ├── disseminator.js │ ├── error.js │ ├── failure-detector.js │ ├── member.js │ ├── membership.js │ ├── message-type.js │ ├── net.js │ └── swim.js ├── package.json └── test/ ├── codec.js ├── disseminator.js ├── failure-detector.js ├── index.js ├── membership.js └── net.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ node_modules/ ================================================ FILE: .jscsrc ================================================ { "fileExtensions": [".js"], "maximumLineLength": 120, "validateIndentation": 4, "validateLineBreaks": "LF", "validateQuoteMarks": "'", "disallowAnonymousFunctions": true, "disallowMultipleLineBreaks": true, "disallowMultipleVarDecl": true, "disallowQuotedKeysInObjects": true, "disallowSpacesInsideObjectBrackets": "all", "excludeFiles": [ "node_modules/**" ], // crockford preset with exceptions "requireCurlyBraces": [ "if", "else", "for", "while", "do", "try", "catch" ], "requireSpaceAfterKeywords": [ "if", "else", "for", "while", "do", "switch", "case", "return", "try", "catch", "function", "typeof" ], "requireSpaceBeforeBlockStatements": true, "requireParenthesesAroundIIFE": true, "requireSpacesInConditionalExpression": true, "disallowSpacesInNamedFunctionExpression": { "beforeOpeningRoundBrace": true }, "disallowSpacesInFunctionDeclaration": { "beforeOpeningRoundBrace": true }, // "requireMultipleVarDecl": "onevar", "requireBlocksOnNewline": true, "disallowEmptyBlocks": true, "disallowSpacesInsideArrayBrackets": true, "disallowSpacesInsideParentheses": true, "disallowDanglingUnderscores": true, "requireCommaBeforeLineBreak": true, "disallowSpaceAfterPrefixUnaryOperators": true, "disallowSpaceBeforePostfixUnaryOperators": true, "disallowSpaceBeforeBinaryOperators": [ "," ], "requireSpacesInForStatement": true, "requireSpaceBeforeBinaryOperators": true, "requireSpaceAfterBinaryOperators": true, "disallowKeywords": [ "with", "continue" ], "validateIndentation": 4, "disallowMixedSpacesAndTabs": true, "disallowTrailingWhitespace": true, "disallowTrailingComma": true, "disallowKeywordsOnNewLine": [ "else" ], "requireLineFeedAtFileEnd": true, "requireCapitalizedConstructors": true, "requireDotNotation": true, "disallowNewlineBeforeBlockStatements": true, "disallowMultipleLineStrings": true } ================================================ FILE: .jshintrc ================================================ { "bitwise": true, "camelcase": true, "curly": true, "eqeqeq": true, "esnext": true, "freeze": true, "immed": true, "indent": 4, "latedef": "nofunc", "maxlen": 120, "newcap": true, "noarg": true, "noempty": true, "nonew": true, "quotmark": "single", "strict": true, "undef": true, "unused": true, "node": true } ================================================ FILE: .npmignore ================================================ # Do not package non-functional code when importing this package via # npm. docs/ bench/ test/ .travis.yml .jscsrc .jshint.rc ================================================ FILE: .travis.yml ================================================ language: node_js node_js: - "node" - "lts/*" env: - CXX=g++-4.8 addons: apt: sources: - ubuntu-toolchain-r-test packages: - g++-4.8 ================================================ FILE: README.md ================================================ # swim-js [![Build Status](https://travis-ci.org/mrhooray/swim-js.svg?branch=master)](https://travis-ci.org/mrhooray/swim-js) > JavaScript implementation of [SWIM](http://www.cs.cornell.edu/~asdas/research/dsn02-SWIM.pdf) membership protocol * [npm](https://www.npmjs.com/package/swim) * [Motivation](#motivation) * [Usage](#usage) * [Benchmark](#benchmark) * [License](#license) ## Motivation Membership management is important to distributed systems and large clusters need a decentralized protocol such as SWIM, which handles failure detection and membership dissemination in a scalable and weakly-consistent way. It can be used to implement functionalities based on membership like distributed consensus, application layer sharding, log replication, etc. ## Usage Installation ```sh npm install swim --save ``` ```js var Swim = require('swim'); var opts = { local: { host: '10.31.1.191:11000', meta: {'application': 'info'} // optional }, codec: 'msgpack', // optional disseminationFactor: 15, // optional interval: 100, // optional joinTimeout: 200, // optional pingTimeout: 20, // optional pingReqTimeout: 60, // optional pingReqGroupSize: 3, // optional suspectTimeout: 60, // optional udp: {maxDgramSize: 512}, // optional preferCurrentMeta: true // optional }; var swim = new Swim(opts); var hostsToJoin = ['10.31.1.192:11000', '10.31.1.193:11000']; swim.bootstrap(hostsToJoin, function onBootstrap(err) { if (err) { // error handling return; } // ready console.log(swim.whoami()); console.log(swim.members()); console.log(swim.checksum()); // change on membership, e.g. new node or node died/left swim.on(Swim.EventType.Change, function onChange(update) {}); // update on membership, e.g. node recovered or update on meta data swim.on(Swim.EventType.Update, function onUpdate(update) {}); // shutdown swim.leave(); }); // or swim.bootstrap(hostsToJoin); // bootstrap error handling swim.on(Swim.EventType.Error, function onError(err) {}); // bootstrap ready swim.on(Swim.EventType.Ready, function onReady() {}); ``` [Additional API Documentation](docs/api.md) ## Benchmark Benchmark convergence time under different configuration ```sh node bench/script/convergence-time.js -h Usage: convergence-time [options] Options: -h, --help output usage information --cycles [value] number of cycles --workers [value] number of workers --codec [value] msgpack or json --dissemination-factor [value] dissemination factor --interval [value] interval --join-timeout [value] join timeout --ping-timeout [value] ping timeout --ping-req-timeout [value] ping req timeout --ping-req-group-size [value] ping req group size --max-dgram-size [value] max dgram size ``` ```sh node bench/script/convergence-time.js configuration: - cycles 10 - workers 10 - codec msgpack - dissemination factor 15 - interval 20 ms - join timeout 100 ms - ping timeout 4 ms - ping req timeout 12 ms - ping req group size 3 - max dgram size 512 bytes convergence time under single node failure histogram data: - count 10 - min 76 - max 123 - mean 100 - median 101 - variance 308.44444444444446 - std dev 17.56258649642599 - p75 116.25 - p95 123 - p99 123 ``` ## License MIT ================================================ FILE: bench/index.js ================================================ 'use strict'; // var cycles = [10, 20]; // var workers = [4, 16, 32, 64]; // var codec = ['json', 'msgpack']; // var disseminationFactor = [4, 16, 32]; // var interval = [10, 100]; // var pingTimeout = [4, 16]; // var pingReqTimeout = [12, 48]; // var pingReqGroupSize = [1, 4]; // var maxDgramSize = [512, 1024, 2048]; ================================================ FILE: bench/lib/runner.js ================================================ 'use strict'; var async = require('async'); var events = require('events'); var util = require('util'); function Runner(opts) { this.cycles = opts.cycles || 0; this.setup = opts.setup || noop; this.teardown = opts.teardown || noop; this.before = opts.suite.before || noop; this.fn = opts.suite.fn || noop; this.after = opts.suite.after || noop; } util.inherits(Runner, events.EventEmitter); Runner.prototype.run = function run(callback) { var self = this; async.series([ function setup(callback) { self.emit(Runner.EventType.Setup); self.setup(callback); }, function run(callback) { async.timesSeries(self.cycles, function wrappedRun(i, callback) { async.series([ function before(callback) { self.emit(Runner.EventType.Before); self.before(callback); }, function fn(callback) { self.emit(Runner.EventType.Fn); self.fn(callback); }, function after(callback) { self.emit(Runner.EventType.After); self.after(callback); } ], callback); }, callback); }, function teardown(callback) { self.emit(Runner.EventType.Teardown); self.teardown(callback); } ], callback); }; Runner.EventType = { After: 'after', Before: 'before', Fn: 'fn', Setup: 'setup', Teardown: 'teardown' }; module.exports = Runner; function noop(callback) { if (typeof callback === 'function') { process.nextTick(callback); } } ================================================ FILE: bench/scenario/single-node-failure.js ================================================ 'use strict'; module.exports = function singleNodeFailure(context, callback) { var hosts = Object.keys(context.hostToAliveWorker); var host = hosts[Math.floor(Math.random() * hosts.length)]; if (host) { context.hostToFaultyWorker[host] = context.hostToAliveWorker[host]; delete context.hostToAliveWorker[host]; context.hostToFaultyWorker[host].send({ cmd: 'leave' }); } process.nextTick(callback); }; ================================================ FILE: bench/script/convergence-time.js ================================================ #!/usr/bin/env node 'use strict'; var cp = require('child_process'); var metrics = require('metrics'); var program = require('commander'); var Runner = require('../lib/runner'); var singleNodeFailure = require('../scenario/single-node-failure'); var PORT_BASE = 20000; if (require.main === module) { parseArgs(); main(); } function parseArgs() { program .option('--cycles [value]', 'number of cycles', parseInt, 10) .option('--workers [value]', 'number of workers', parseInt, 10) .option('--codec [value]', 'msgpack or json', 'msgpack') .option('--dissemination-factor [value]', 'dissemination factor', parseInt, 15) .option('--interval [value]', 'interval', parseInt, 20) .option('--join-timeout [value]', 'join timeout', parseInt, 100) .option('--ping-timeout [value]', 'ping timeout', parseInt, 4) .option('--ping-req-timeout [value]', 'ping req timeout', parseInt, 12) .option('--ping-req-group-size [value]', 'ping req group size', parseInt, 3) .option('--max-dgram-size [value]', 'max dgram size', parseInt, 512) .parse(process.argv); console.log('configuration:'); console.log('- cycles', program.cycles); console.log('- workers', program.workers); console.log('- codec', program.codec); console.log('- dissemination factor', program.disseminationFactor); console.log('- interval', program.interval, 'ms'); console.log('- join timeout', program.joinTimeout, 'ms'); console.log('- ping timeout', program.pingTimeout, 'ms'); console.log('- ping req timeout', program.pingReqTimeout, 'ms'); console.log('- ping req group size', program.pingReqGroupSize); console.log('- max dgram size', program.maxDgramSize, 'bytes'); } function main() { var histogram = new metrics.Histogram(); var context = { hostToAliveWorker: Object.create(null), hostToFaultyWorker: Object.create(null), hostToChecksum: Object.create(null), numberOfWorkers: program.workers }; var runner = new Runner({ cycles: program.cycles, setup: setup.bind(undefined, context), teardown: teardown.bind(undefined, context), suite: { before: before.bind(undefined, context), fn: fn.bind(undefined, context), after: after.bind(undefined, context) } }); var time; runner.on(Runner.EventType.Fn, function onCycleStart() { time = Date.now(); }); runner.on(Runner.EventType.After, function onCycleComplete() { histogram.update(Date.now() - time); }); console.log('convergence time under single node failure'); runner.run(function report() { var result = histogram.printObj(); console.log('histogram data:'); console.log('- count', result.count); console.log('- min', result.min); console.log('- max', result.max); console.log('- mean', result.mean); console.log('- median', result.median); console.log('- variance', result.variance); /* jshint camelcase: false */ console.log('- std dev', result.std_dev); /* jshint camelcase: true */ console.log('- p75', result.p75); console.log('- p95', result.p95); console.log('- p99', result.p99); }); } function setup(context, callback) { var readyCount = 0; fork(context, function onMessage(message) { switch (message.type) { case 'ready': readyCount += 1; if (readyCount === context.numberOfWorkers) { Object.keys(context.hostToAliveWorker).forEach(function join(host) { context.hostToAliveWorker[host].send({ cmd: 'join', hosts: getHostsToJoin(Math.ceil(context.numberOfWorkers / 3)) }); }); waitForConvergence(context, callback); } break; case 'checksum': context.hostToChecksum[message.host] = message.value; break; } }); } function fork(context, onMessage) { var args; var host; var worker; var i; for (i = 0; i < context.numberOfWorkers; i++) { host = '127.0.0.1:' + (PORT_BASE + i); args = []; args.push('--host', host); if (program.codec) { args.push('--codec', program.codec); } if (program.disseminationFactor) { args.push('--dissemination-factor', program.disseminationFactor); } if (program.interval) { args.push('--interval', program.interval); } if (program.joinTimeout) { args.push('--join-timeout', program.joinTimeout); } if (program.pingTimeout) { args.push('--ping-timeout', program.pingTimeout); } if (program.pingReqTimeout) { args.push('--ping-req-timeout', program.pingReqTimeout); } if (program.pingReqGroupSize) { args.push('--ping-req-group-size', program.pingReqGroupSize); } if (program.maxDgramSize) { args.push('--max-dgram-size', program.maxDgramSize); } worker = cp.fork(__dirname + '/worker.js', args); worker.on('message', onMessage); context.hostToAliveWorker[host] = worker; } } function getHostsToJoin(n) { var hostToJoin = []; var i; for (i = 0; i < n; i++) { hostToJoin.push('127.0.0.1:' + (PORT_BASE + i)); } return hostToJoin; } function waitForConvergence(context, callback) { var handle = setInterval(function check() { var hosts = Object.keys(context.hostToAliveWorker); var i; for (i = 1; i < hosts.length; i++) { if (!context.hostToChecksum[hosts[i]] || context.hostToChecksum[hosts[i - 1]] !== context.hostToChecksum[hosts[i]]) { return; } } if (Object.keys(context.hostToChecksum).length >= Object.keys(context.hostToAliveWorker).length) { context.hostToChecksum = Object.create(null); clearInterval(handle); callback(); } }, 5); } function teardown(context, callback) { Object.keys(context.hostToAliveWorker).forEach(function shutdown(host) { context.hostToAliveWorker[host].send({ cmd: 'shutdown' }); }); process.nextTick(callback); } function before(context, callback) { singleNodeFailure(context, callback); } function fn(context, callback) { waitForConvergence(context, callback); } function after(context, callback) { Object.keys(context.hostToFaultyWorker).forEach(function join(host) { context.hostToAliveWorker[host] = context.hostToFaultyWorker[host]; delete context.hostToFaultyWorker[host]; context.hostToAliveWorker[host].send({ cmd: 'bootstrap', hosts: getHostsToJoin(Math.ceil(context.numberOfWorkers / 3)) }); }); waitForConvergence(context, callback); } ================================================ FILE: bench/script/dumb.js ================================================ #!/usr/bin/env node 'use strict'; var Swim = require('../../lib/swim'); var hosts = [ '127.0.0.1:11111', '127.0.0.1:22222', '127.0.0.1:33333' ]; var swim0 = new Swim({ local: { host: hosts[0], meta: { node: 1 } } }); var swim1 = new Swim({ local: { host: hosts[1], meta: { node: 2 } } }); var swim2 = new Swim({ local: { host: hosts[2], meta: { node: 3 } } }); swim0.bootstrap([], function onBootstrap(err) { console.log('swim0 bootstrap error', err); swim1.bootstrap(hosts, function onBootstrap(err) { console.log('swim1 bootstrap error', err); }); swim2.bootstrap(hosts, function onBootstrap(err) { console.log('swim2 bootstrap error', err); }); }); setInterval(function print() { console.log('-------------------------------'); console.log(swim0.localhost(), swim0.members()); console.log(swim1.localhost(), swim1.members()); console.log(swim2.localhost(), swim2.members()); }, 1000); setTimeout(function leave() { console.log('swim0 leaves'); swim0.leave(); }, 1000 * 5); setTimeout(function rejoin() { console.log('swim0 rejoins'); swim0.bootstrap(hosts, function onBootstrap(err) { console.log('swim0 bootstrap error', err); }); }, 1000 * 15); setTimeout(function leave() { console.log('swim0 leaves'); swim0.leave(); }, 1000 * 20); setTimeout(function rejoin() { console.log('swim0 rejoins'); swim0.bootstrap(hosts, function onBootstrap(err) { console.log('swim0 bootstrap error', err); }); }, 1000 * 23); ================================================ FILE: bench/script/worker.js ================================================ #!/usr/bin/env node 'use strict'; var assert = require('assert'); var program = require('commander'); var Swim = require('../../'); if (require.main === module) { parseArgs(); bootstrap(function onBootstrap(err, swim) { if (err) { console.log(err); process.exit(1); } handleMessage(swim); }); } function parseArgs() { program .option('--host ', 'host') .option('--hosts-to-join [hosts]', 'hosts to join', function split(list) { return list.split(','); }, []) .option('--codec [value]', 'msgpack or json') .option('--dissemination-factor [value]', 'dissemination factor', parseInt) .option('--interval [value]', 'interval', parseInt) .option('--join-timeout [value]', 'join timeout', parseInt) .option('--ping-timeout [value]', 'ping timeout', parseInt) .option('--ping-req-timeout [value]', 'ping req timeout', parseInt) .option('--ping-req-group-size [value]', 'ping req group size', parseInt) .option('--max-dgram-size [value]', 'max dgram size', parseInt) .parse(process.argv); assert(/^(\d+\.\d+\.\d+\.\d+):(\d+)$/.test(program.host)); assert(Array.isArray(program.hostsToJoin)); } function handleMessage(swim) { var onMessage = function onMessage(message) { switch (message.cmd) { case 'bootstrap': swim.bootstrap(message.hosts, function onBootstrap(err) { if (err) { console.log(err); process.exit(1); } process.send({ type: 'checksum', host: swim.localhost(), value: swim.checksum() }); }); break; case 'join': swim.join(message.hosts, function onBootstrap(err) { if (err) { console.log(err); process.exit(1); } process.send({ type: 'checksum', host: swim.localhost(), value: swim.checksum() }); }); break; case 'leave': swim.leave(); break; case 'shutdown': swim.leave(); process.removeListener('message', onMessage); break; } }; process.on('message', onMessage); } function bootstrap(callback) { var opts = { local: { host: program.host, meta: { app: 'benchmark' } }, codec: program.codec, disseminationFactor: program.disseminationFactor, interval: program.interval, joinTimeout: program.joinTimeout, pingTimeout: program.pingTimeout, pingReqTimeout: program.pingReqTimeout, pingReqGroupSize: program.pingReqGroupSize, udp: { maxDgramSize: program.maxDgramSize } }; var swim = new Swim(opts); swim.on(Swim.EventType.Update, function onUpdate() { process.send({ type: 'checksum', host: swim.localhost(), value: swim.checksum() }); }); swim.bootstrap(program.hostsToJoin, function onBootstrap(err) { if (err) { return callback(err); } process.send({ type: 'checksum', host: swim.localhost(), value: swim.checksum() }); process.send({ type: 'ready' }); callback(null, swim); }); } ================================================ FILE: docs/api.md ================================================ # API Documentation ## Swim object ### Constructor(options) See Options Object. ### boostrap(hosts, callback) Bootstrap swim instance and join cluster. * hosts - address:port list to connect to when joining the cluster * callback(err) - callback function ### join(hosts, callback) Join cluster. * hosts - address:port list to connect to when joining the cluster * callback(err) - callback function ### leave() Leave cluster. ### localhost() Get host for local node. ### whoami() Alias to localhost(). ### members(hasLocal, hasFaulty) Get members in the cluster. * hasLocal - include local node * hasFaulty - include nodes marked as faulty ### checksum() Get membership checksum. ### updateMeta(meta) Update metadata of local node and disseminate within cluster. ## Options Object This object contains configuration settings. ### local Local Option. * local.host (required) - address:port for this instance, e.g. 127.0.0.1:11000 * local.meta (optional) - metadata about this node, which will be disseminated within cluster ### codec Codec of message payload. Default: msgpack. * json - https://www.json.org/ * msgpack - https://msgpack.org/ ### disseminationFactor Dissemination factor can be used to fine tune the responsiveness of the cluster. Greater dissemination factor results to: * more hosts being notified in every round of dissemination * lower convergence time of cluster membership * more/bigger network packets being sent and vice versa. ### interval Number of milliseconds between failure detections, also known as the protocol interval. Every X milliseconds, nodes will ping a member of the SWIM network to check its liveness with Time-Bounded Strong Completeness as described in the [paper](http://www.cs.cornell.edu/~asdas/research/dsn02-SWIM.pdf). ### joinTimeout Number of milliseconds before emitting a JoinTimeout error. The node will still run as a base node separate from the network. ### pingTimout Number of milliseconds before sending ping-req messages to the unresponsive node. ### pingReqTimout Number of milliseconds elapsed from sending ping-req message before marking the unresponsive node suspicious. ### pingReqGroupSize Number of hosts to send ping-req messages to for pinging unresponsive nodes indirectly to reduce false positives. ### suspectTimeout Number of milliseconds before considering a suspect node faulty. ### udp UDP Option. * udp.maxDgramSize - Max size of UDP datagram. If bigger than what the network supports, messages might be chunked into multiple packets and discarded at receiver end. ### preferCurrentMeta If set to true, current metadata of local node, instead of the copy of metadata of local node in cluster membership, will be used during conflict resolution. Defaut: false. ================================================ FILE: index.js ================================================ 'use strict'; module.exports = require('./lib/swim'); ================================================ FILE: lib/codec.js ================================================ 'use strict'; var assert = require('assert'); var msgpack = require('msgpack'); function Codec(opts) { opts = opts || {}; this.codec = opts.codec || Codec.Default.codec; assert.strictEqual(['json', 'msgpack'].indexOf(this.codec) !== -1, true, 'unsupported codec'); if (this.codec === 'json') { this.encode = function encode(obj) { return new Buffer(JSON.stringify(obj)); }; this.decode = function decode(buffer) { return JSON.parse(buffer.toString()); }; } else { this.encode = msgpack.pack.bind(msgpack); this.decode = msgpack.unpack.bind(msgpack); } } Codec.Default = { codec: 'msgpack' }; module.exports = Codec; ================================================ FILE: lib/disseminator.js ================================================ 'use strict'; var debug = require('debug'); var clone = require('clone'); var events = require('events'); var util = require('util'); var Membership = require('./membership'); var MessageType = require('./message-type'); var Net = require('./net'); function Disseminator(opts) { this.swim = opts.swim; this.disseminationFactor = opts.disseminationFactor || Disseminator.Default.disseminationFactor; this.disseminationFormula = opts.disseminationFormula || Disseminator.Default.disseminationFormula; this.disseminationLimit = Disseminator.Default.disseminationLimit; this.updateListener = this.onUpdate.bind(this); this.changeListener = this.onChange.bind(this); this.attemptsToUpdates = Object.create(null); this.hostToAttempts = Object.create(null); this.debug = debug('swim:disseminator').bind(undefined, opts.debugIdentifier); } util.inherits(Disseminator, events.EventEmitter); Disseminator.prototype.start = function start() { this.swim.membership.on(Membership.EventType.Change, this.changeListener); this.swim.membership.on(Membership.EventType.Update, this.updateListener); this.updateDisseminationLimit(); }; Disseminator.prototype.stop = function stop() { this.swim.membership.removeListener(Membership.EventType.Change, this.changeListener); this.swim.membership.removeListener(Membership.EventType.Update, this.updateListener); }; Disseminator.prototype.onChange = function onChange() { this.updateDisseminationLimit(); }; Disseminator.prototype.updateDisseminationLimit = function updateDisseminationLimit() { var self = this; self.disseminationLimit = self.disseminationFormula(self.disseminationFactor, self.swim.membership.size(true)); Object.keys(self.attemptsToUpdates).forEach(function removeAttempts(attempts) { if (attempts >= self.disseminationLimit) { Object.keys(self.attemptsToUpdates[attempts]).forEach(function removeUpdate(host) { delete self.hostToAttempts[host]; }); delete self.attemptsToUpdates[attempts]; } }); }; Disseminator.prototype.onUpdate = function onUpdate(data) { var update = clone(data); update.attempts = 0; this.removeUpdate(update); this.addUpdate(update); }; Disseminator.prototype.addUpdate = function addUpdate(update) { if (update.attempts >= this.disseminationLimit) { return; } if (!this.attemptsToUpdates[update.attempts]) { this.attemptsToUpdates[update.attempts] = Object.create(null); } this.attemptsToUpdates[update.attempts][update.host] = update; this.hostToAttempts[update.host] = update.attempts; }; Disseminator.prototype.removeUpdate = function removeUpdate(update) { if (this.hostToAttempts[update.host] >= 0) { delete this.attemptsToUpdates[this.hostToAttempts[update.host]][update.host]; } delete this.hostToAttempts[update.host]; }; Disseminator.prototype.getUpdatesUpTo = function getUpdatesUpTo(bytesAvailable) { var self = this; var buffers = []; var updates = []; var hostToUpdates; var attempts; var buffer; var update; for (attempts = 0; attempts < self.disseminationLimit; attempts++) { if (bytesAvailable <= Net.MessageTypeSize) { break; } hostToUpdates = self.attemptsToUpdates[attempts]; if (hostToUpdates) { /* jshint loopfunc: true */ Object.keys(hostToUpdates).forEach(function disseminateUpdateOf(host) { if (bytesAvailable <= Net.MessageTypeSize) { return; } update = hostToUpdates[host]; buffer = self.serializeUpdate(update); if (buffer.length + Net.LengthSize <= bytesAvailable) { buffers.push(buffer); updates.push(update); self.removeUpdate(update); bytesAvailable -= buffer.length + Net.LengthSize; } }); /* jshint loopfunc: false */ } } updates.forEach(function addBack(update) { update.attempts += 1; self.addUpdate(update); }); return buffers; }; Disseminator.prototype.serializeUpdate = function serializeUpdate(update) { var header = new Buffer(Net.MessageTypeSize); Net.WriteMessageType.call(header, MessageType.Update, 0); return Buffer.concat([header, this.swim.codec.encode({ meta: update.meta, host: update.host, state: update.state, incarnation: update.incarnation })]); }; Disseminator.Default = { disseminationFactor: 15, disseminationLimit: 3, disseminationFormula: function disseminationFormula(factor, size) { return Math.ceil(factor * Math.log(size + 1) / Math.log(10)); } }; module.exports = Disseminator; ================================================ FILE: lib/error.js ================================================ 'use strict'; var util = require('util'); function JoinFailedError(meta) { Error.captureStackTrace(this, this.constructor); Object.defineProperty(this, 'name', { value: this.constructor.name }); if (meta) { this.meta = meta; } } function InvalidStateError(meta) { Error.captureStackTrace(this, this.constructor); Object.defineProperty(this, 'name', { value: this.constructor.name }); if (meta) { this.meta = meta; } } function ListenFailedError(meta) { Error.captureStackTrace(this, this.constructor); Object.defineProperty(this, 'name', { value: this.constructor.name }); if (meta) { this.meta = meta; } } util.inherits(JoinFailedError, Error); util.inherits(InvalidStateError, Error); util.inherits(ListenFailedError, Error); module.exports = { JoinFailedError: JoinFailedError, InvalidStateError: InvalidStateError, ListenFailedError: ListenFailedError }; ================================================ FILE: lib/failure-detector.js ================================================ 'use strict'; var debug = require('debug'); var events = require('events'); var util = require('util'); var MessageType = require('./message-type'); var Net = require('./net'); function FailureDetector(opts) { this.swim = opts.swim; this.interval = opts.interval || FailureDetector.Default.interval; this.pingTimeout = opts.pingTimeout || FailureDetector.Default.pingTimeout; this.pingReqTimeout = opts.pingReqTimeout || FailureDetector.Default.pingReqTimeout; this.pingReqGroupSize = opts.pingReqGroupSize || FailureDetector.Default.pingReqGroupSize; this.seq = 0; this.pingListener = this.onPing.bind(this); this.pingReqListener = this.onPingReq.bind(this); this.ackListener = this.onAck.bind(this); this.tickHandle = undefined; this.seqToTimeout = Object.create(null); this.seqToCallback = Object.create(null); this.debug = debug('swim:failure-detector').bind(undefined, opts.debugIdentifier); } util.inherits(FailureDetector, events.EventEmitter); FailureDetector.prototype.start = function start() { this.swim.net.on(Net.EventType.Ping, this.pingListener); this.swim.net.on(Net.EventType.PingReq, this.pingReqListener); this.swim.net.on(Net.EventType.Ack, this.ackListener); this.tick(); }; FailureDetector.prototype.stop = function stop() { var self = this; clearInterval(self.tickHandle); self.tickHandle = undefined; self.swim.net.removeListener(Net.EventType.Ping, self.pingListener); self.swim.net.removeListener(Net.EventType.PingReq, self.pingReqListener); self.swim.net.removeListener(Net.EventType.Ack, self.ackListener); Object.keys(self.seqToTimeout).forEach(function clearTimeoutWithDeletion(seq) { clearTimeout(self.seqToTimeout[seq]); delete self.seqToTimeout[seq]; }); Object.keys(self.seqToCallback).forEach(function clearCallback(seq) { delete self.seqToCallback[seq]; }); }; FailureDetector.prototype.tick = function tick() { setImmediate(this.ping.bind(this)); this.tickHandle = setInterval(this.ping.bind(this), this.interval); }; FailureDetector.prototype.ping = function ping() { this.pingMember(this.swim.membership.next()); }; FailureDetector.prototype.pingMember = function pingMember(member) { var self = this; var seq = self.seq; if (!member) { return; } self.seq += 1; self.seqToTimeout[seq] = setTimeout(function receiveTimeout() { self.clearSeq(seq); self.pingReq(member); }, self.pingTimeout); self.swim.net.sendMessage({ type: MessageType.Ping, data: { seq: seq } }, member.host); }; FailureDetector.prototype.pingReq = function pingReq(member) { var self = this; var relayMembers = self.swim.membership.random(self.pingReqGroupSize); var timeout; if (relayMembers.length === 0) { return; } timeout = setTimeout(function pingReqTimeout() { self.emit(FailureDetector.EventType.Suspect, member); }, self.pingReqTimeout); relayMembers.forEach(function pingThrough(relayMember) { self.pingReqThroughMember(member, relayMember, function pingReqThroughMemberCallback() { clearTimeout(timeout); }); }); }; FailureDetector.prototype.pingReqThroughMember = function pingReqThroughMember(member, relayMember, callback) { var self = this; var seq = self.seq; self.seq += 1; self.seqToTimeout[seq] = setTimeout(function receiveTimeout() { self.clearSeq(seq); }, self.pingReqTimeout); self.seqToCallback[seq] = function pingReqAckReceiveCallback() { self.clearSeq(seq); callback.apply(undefined, arguments); }; self.swim.net.sendMessage({ type: MessageType.PingReq, data: { seq: seq, destination: member.host } }, relayMember.host); }; FailureDetector.prototype.onPing = function onPing(data, host) { this.swim.net.sendMessage({ type: MessageType.Ack, data: { seq: data.seq } }, host); }; FailureDetector.prototype.onPingReq = function onPingReq(data, host) { var self = this; var seq = self.seq; self.seq += 1; self.seqToTimeout[seq] = setTimeout(function receiveTimeout() { self.clearSeq(seq); }, self.pingTimeout); self.seqToCallback[seq] = function pingAckReceiveCallback() { self.clearSeq(seq); self.swim.net.sendMessage({ type: MessageType.Ack, data: { seq: data.seq } }, host); }; self.swim.net.sendMessage({ type: MessageType.Ping, data: { seq: seq } }, data.destination); }; FailureDetector.prototype.onAck = function onAck(data) { var callback = this.seqToCallback[data.seq]; if (callback) { process.nextTick(callback); } this.clearSeq(data.seq); }; FailureDetector.prototype.clearSeq = function clearSeq(seq) { clearTimeout(this.seqToTimeout[seq]); delete this.seqToCallback[seq]; delete this.seqToTimeout[seq]; }; FailureDetector.Default = { interval: 20, pingTimeout: 4, pingReqTimeout: 12, pingReqGroupSize: 3 }; FailureDetector.EventType = { Suspect: 'suspect' }; module.exports = FailureDetector; ================================================ FILE: lib/member.js ================================================ 'use strict'; var clone = require('clone'); function Member(opts) { this.meta = opts.meta || undefined; this.host = opts.host; this.state = opts.state || Member.State.Alive; this.incarnation = opts.incarnation || 0; } Member.prototype.data = function data() { return clone({ meta: this.meta, host: this.host, state: this.state, incarnation: this.incarnation }); }; Member.prototype.incarnate = function incarnate(data, force, preferCurrentMeta) { if (!data) { this.incarnation += 1; return true; } if (data.incarnation > this.incarnation) { if (!preferCurrentMeta) { this.meta = data.meta; } this.incarnation = data.incarnation + 1; return true; } if (data.incarnation === this.incarnation && force) { this.incarnation = data.incarnation + 1; return true; } return false; }; Member.State = { Alive: 0, Suspect: 1, Faulty: 2 }; module.exports = Member; ================================================ FILE: lib/membership.js ================================================ 'use strict'; var debug = require('debug'); var events = require('events'); var farmhash = require('farmhash'); var util = require('util'); var FailureDetector = require('./failure-detector'); var Member = require('./member'); var MessageType = require('./message-type'); var Net = require('./net'); function Membership(opts) { this.swim = opts.swim; this.local = new Member(opts.local); this.suspectTimeout = opts.suspectTimeout || Membership.Default.suspectTimeout; this.preferCurrentMeta = opts.preferCurrentMeta || Membership.Default.preferCurrentMeta; this.ackListener = this.onAck.bind(this); this.updateListener = this.onUpdate.bind(this); this.suspectListener = this.onSuspect.bind(this); this.syncListener = this.onSync.bind(this); this.hostToMember = Object.create(null); this.hostToIterable = Object.create(null); this.hostToFaulty = Object.create(null); this.hostToSuspectTimeout = Object.create(null); this.debug = debug('swim:membership').bind(undefined, opts.debugIdentifier); } util.inherits(Membership, events.EventEmitter); Membership.prototype.start = function start() { var self = this; self.swim.failureDetector.on(FailureDetector.EventType.Suspect, self.suspectListener); self.swim.net.on(Net.EventType.Ack, self.ackListener); self.swim.net.on(Net.EventType.Sync, self.syncListener); self.swim.net.on(Net.EventType.Update, self.updateListener); Object.keys(self.hostToSuspectTimeout).forEach(function resumeSuspect(host) { self.onSuspect(self.get(host)); }); }; Membership.prototype.stop = function stop() { var self = this; self.swim.failureDetector.removeListener(FailureDetector.EventType.Suspect, self.suspectListener); self.swim.net.removeListener(Net.EventType.Ack, self.ackListener); self.swim.net.removeListener(Net.EventType.Sync, self.syncListener); self.swim.net.removeListener(Net.EventType.Update, self.updateListener); Object.keys(self.hostToSuspectTimeout).forEach(function clearTimeoutWithoutDeletion(host) { clearTimeout(self.hostToSuspectTimeout[host]); }); }; Membership.prototype.onAck = function onAck(data, host) { if (this.hostToMember[host] && this.hostToMember[host].state === Member.State.Suspect) { this.swim.net.sendMessage({ type: MessageType.Update, data: this.hostToMember[host].data() }, host); } }; Membership.prototype.onSuspect = function onSuspect(member) { var self = this; var data; member = new Member(member.data()); member.state = Member.State.Suspect; data = member.data(); clearTimeout(self.hostToSuspectTimeout[data.host]); delete self.hostToSuspectTimeout[data.host]; self.hostToSuspectTimeout[data.host] = setTimeout(function setFaulty() { delete self.hostToSuspectTimeout[data.host]; data.state = Member.State.Faulty; self.onUpdate(data); }, self.suspectTimeout); self.onUpdate(member.data()); }; Membership.prototype.onSync = function onSync(data) { var host = data.host; this.updateAlive(data); this.swim.net.sendMessages(this.all(true, true).map(function toMessage(data) { return { type: MessageType.Update, data: data }; }), host); }; Membership.prototype.sync = function sync(hosts) { var self = this; var messages = [{ type: MessageType.Sync, data: self.local.data() }]; this.all(false, true).forEach(function addMessage(data) { messages.push({ type: MessageType.Update, data: data }); }); hosts.forEach(function sendToHost(host) { self.swim.net.sendMessages(messages, host); }); }; Membership.prototype.onUpdate = function onUpdate(data) { this.debug('received update', data); switch (data.state) { case Member.State.Alive: this.updateAlive(data); break; case Member.State.Suspect: this.updateSuspect(data); break; case Member.State.Faulty: this.updateFaulty(data); break; } }; Membership.prototype.updateAlive = function updateAlive(data) { if (this.isLocal(data.host)) { if (this.local.incarnate(data, false, this.preferCurrentMeta)) { this.emit(Membership.EventType.Update, this.local.data()); } else { this.emit(Membership.EventType.Drop, data); } return; } if (this.hostToFaulty[data.host] && this.hostToFaulty[data.host].incarnation >= data.incarnation) { this.emit(Membership.EventType.Drop, data); return; } if (!this.hostToMember[data.host] || data.incarnation > this.hostToMember[data.host].incarnation) { clearTimeout(this.hostToSuspectTimeout[data.host]); delete this.hostToSuspectTimeout[data.host]; delete this.hostToFaulty[data.host]; if (!this.hostToMember[data.host]) { this.hostToMember[data.host] = new Member(data); this.hostToIterable[data.host] = this.hostToMember[data.host]; this.emit(Membership.EventType.Change, this.hostToMember[data.host].data()); } else { this.hostToMember[data.host] = new Member(data); } this.emit(Membership.EventType.Update, this.hostToMember[data.host].data()); } else { this.emit(Membership.EventType.Drop, data); } }; Membership.prototype.updateSuspect = function updateSuspect(data) { if (this.isLocal(data.host)) { this.emit(Membership.EventType.Drop, data); this.local.incarnate(data, true, this.preferCurrentMeta); this.emit(Membership.EventType.Update, this.local.data()); return; } if (this.hostToFaulty[data.host] && this.hostToFaulty[data.host].incarnation >= data.incarnation) { this.emit(Membership.EventType.Drop, data); return; } if (!this.hostToMember[data.host] || data.incarnation > this.hostToMember[data.host].incarnation || data.incarnation === this.hostToMember[data.host].incarnation && this.hostToMember[data.host].state === Member.State.Alive) { delete this.hostToFaulty[data.host]; if (!this.hostToMember[data.host]) { this.hostToMember[data.host] = new Member(data); this.hostToIterable[data.host] = this.hostToMember[data.host]; this.emit(Membership.EventType.Change, this.hostToMember[data.host].data()); } else { this.hostToMember[data.host] = new Member(data); } this.emit(Membership.EventType.Update, this.hostToMember[data.host].data()); } else { this.emit(Membership.EventType.Drop, data); } }; Membership.prototype.updateFaulty = function updateFaulty(data) { if (this.isLocal(data.host)) { this.emit(Membership.EventType.Drop, data); this.local.incarnate(data, true, this.preferCurrentMeta); this.emit(Membership.EventType.Update, this.local.data()); return; } if (this.hostToMember[data.host] && data.incarnation >= this.hostToMember[data.host].incarnation) { this.hostToFaulty[data.host] = new Member(data); delete this.hostToMember[data.host]; delete this.hostToIterable[data.host]; this.emit(Membership.EventType.Change, data); this.emit(Membership.EventType.Update, data); } else { this.emit(Membership.EventType.Drop, data); } }; Membership.prototype.next = function next() { var hosts = Object.keys(this.hostToIterable); var host; var member; if (hosts.length === 0) { this.shuffle(); hosts = Object.keys(this.hostToIterable); } host = hosts[Math.floor(Math.random() * hosts.length)]; member = this.hostToIterable[host]; delete this.hostToIterable[host]; return member; }; Membership.prototype.random = function random(n) { var hosts = Object.keys(this.hostToMember); var selected = []; var index; var i; for (i = 0; i < n && i < hosts.length; i++) { index = i + Math.floor(Math.random() * (hosts.length - i)); selected.push(this.hostToMember[hosts[index]]); hosts[index] = hosts[i]; } return selected; }; Membership.prototype.shuffle = function shuffle() { var self = this; self.hostToIterable = Object.create(null); Object.keys(self.hostToMember).forEach(function addToIterable(host) { self.hostToIterable[host] = self.hostToMember[host]; }); }; Membership.prototype.get = function get(host) { return this.hostToMember[host]; }; Membership.prototype.size = function size(hasLocal) { return Object.keys(this.hostToMember).length + (hasLocal ? 1 : 0); }; Membership.prototype.all = function all(hasLocal, hasFaulty) { var self = this; var results = Object.keys(self.hostToMember).map(function toData(host) { return self.hostToMember[host].data(); }); if (hasLocal) { results.push(self.local.data()); } if (hasFaulty) { Object.keys(self.hostToFaulty).forEach(function toData(host) { results.push(self.hostToFaulty[host].data()); }); } return results; }; Membership.prototype.checksum = function checksum() { var self = this; var strs = self.all(true).sort(function compare(a, b) { return parseInt(a.host.split(':')[1]) - parseInt(b.host.split(':')[1]); }).map(function toString(member) { return member.host + member.state + member.incarnation; }); return farmhash.hash64(strs.join('-')); }; Membership.prototype.isLocal = function isLocal(host) { return host === this.local.host; }; Membership.prototype.localhost = function localhost() { return this.local.host; }; Membership.prototype.updateMeta = function updateMeta(meta) { this.local.meta = meta; this.local.incarnate(); this.emit(Membership.EventType.Update, this.local.data()); }; Membership.Default = { suspectTimeout: 10, preferCurrentMeta: false }; Membership.EventType = { Change: 'change', Drop: 'drop', Update: 'update' }; module.exports = Membership; ================================================ FILE: lib/message-type.js ================================================ 'use strict'; module.exports = { Compound: 0, Compressed: 1, Encrypted: 2, Ping: 10, PingReq: 11, Sync: 12, Ack: 13, Update: 14 }; ================================================ FILE: lib/net.js ================================================ 'use strict'; var debug = require('debug'); var dgram = require('dgram'); var events = require('events'); var util = require('util'); var MessageType = require('./message-type'); function Net(opts) { this.swim = opts.swim; this.udp = { port: opts.udp.port, type: opts.udp.type || Net.Default.udp.type, maxDgramSize: opts.udp.maxDgramSize || Net.Default.udp.maxDgramSize }; this.errorListener = this.emit.bind(this, Net.EventType.Error); this.listeningListener = this.emit.bind(this, Net.EventType.Listening); this.messageListener = this.onNetMessage.bind(this); this.udpSocket = dgram.createSocket(this.udp.type); this.debug = debug('swim:net').bind(undefined, opts.debugIdentifier); } util.inherits(Net, events.EventEmitter); Net.prototype.listen = function listen(callback) { this.udpSocket.on('error', this.errorListener); this.udpSocket.on('listening', this.listeningListener); this.udpSocket.on('message', this.messageListener); this.udpSocket.bind(this.udp.port, callback); }; Net.prototype.close = function close() { this.udpSocket.removeListener('error', this.errorListener); this.udpSocket.removeListener('listening', this.listeningListener); this.udpSocket.removeListener('message', this.messageListener); this.udpSocket.close(); this.udpSocket = dgram.createSocket(this.udp.type); }; Net.prototype.onNetMessage = function onNetMessage(buffer, rinfo) { this.debug('received buffer', { from: formatRinfo(rinfo), length: buffer.length }); this.onMessage(buffer, rinfo); }; Net.prototype.onMessage = function onMessage(buffer, rinfo) { if (buffer.length < Net.MessageTypeSize) { return this.onUnknown(buffer, rinfo); } var messageType = Net.ReadMessageType.call(buffer, 0); switch (messageType) { case MessageType.Compound: this.onCompound(buffer.slice(Net.MessageTypeSize), rinfo); break; case MessageType.Ping: this.onPing(buffer.slice(Net.MessageTypeSize), rinfo); break; case MessageType.PingReq: this.onPingReq(buffer.slice(Net.MessageTypeSize), rinfo); break; case MessageType.Sync: this.onSync(buffer.slice(Net.MessageTypeSize), rinfo); break; case MessageType.Ack: this.onAck(buffer.slice(Net.MessageTypeSize), rinfo); break; case MessageType.Update: this.onUpdate(buffer.slice(Net.MessageTypeSize), rinfo); break; default: this.onUnknown(buffer, rinfo); } }; Net.prototype.onCompound = function onCompound(buffer, rinfo) { this.debug('received compound message'); if (buffer.length < Net.LengthSize) { this.debug('cannot parse number of messages in compound message', { from: formatRinfo(rinfo), length: buffer.length, buffer: buffer.toString() }); return; } var numberOfMessages = Net.ReadLength.call(buffer, 0); var readIndex = Net.LengthSize; var length; var i; for (i = 0; i < numberOfMessages; i++) { if (buffer.length - readIndex < Net.LengthSize) { this.debug('cannot parse length of message in compound message', { readIndex: readIndex, from: formatRinfo(rinfo), length: buffer.length, buffer: buffer.toString() }); break; } length = Net.ReadLength.call(buffer, readIndex); readIndex += Net.LengthSize; this.onMessage(buffer.slice(readIndex, readIndex + length), rinfo); readIndex += length; } }; Net.prototype.onPing = function onPing(buffer, rinfo) { var data; try { data = this.swim.codec.decode(buffer); } catch (e) { this.debug('failed to decode data', { stack: e.stack, from: formatRinfo(rinfo), length: buffer.length, buffer: buffer.toString() }); return; } this.debug('received ping message', { from: formatRinfo(rinfo), data: data }); this.emit(Net.EventType.Ping, data, formatRinfo(rinfo)); }; Net.prototype.onPingReq = function onPingReq(buffer, rinfo) { var data; try { data = this.swim.codec.decode(buffer); } catch (e) { this.debug('failed to decode data', { stack: e.stack, from: formatRinfo(rinfo), length: buffer.length, buffer: buffer.toString() }); return; } this.debug('received pingreq message', { from: formatRinfo(rinfo), data: data }); this.emit(Net.EventType.PingReq, data, formatRinfo(rinfo)); }; Net.prototype.onSync = function onSync(buffer, rinfo) { var data; try { data = this.swim.codec.decode(buffer); } catch (e) { this.debug('failed to decode data', { stack: e.stack, from: formatRinfo(rinfo), length: buffer.length, buffer: buffer.toString() }); return; } this.debug('received sync message', { from: formatRinfo(rinfo), data: data }); this.emit(Net.EventType.Sync, data, formatRinfo(rinfo)); }; Net.prototype.onAck = function onAck(buffer, rinfo) { var data; try { data = this.swim.codec.decode(buffer); } catch (e) { this.debug('failed to decode data', { stack: e.stack, from: formatRinfo(rinfo), length: buffer.length, buffer: buffer.toString() }); return; } this.debug('received ack message', { from: formatRinfo(rinfo), data: data }); this.emit(Net.EventType.Ack, data, formatRinfo(rinfo)); }; Net.prototype.onUpdate = function onUpdate(buffer, rinfo) { var data; try { data = this.swim.codec.decode(buffer); } catch (e) { this.debug('failed to decode data', { stack: e.stack, from: formatRinfo(rinfo), length: buffer.length, buffer: buffer.toString() }); return; } this.debug('received update message', { from: formatRinfo(rinfo), data: data }); this.emit(Net.EventType.Update, data, formatRinfo(rinfo)); }; Net.prototype.onUnknown = function onUnknown(buffer, rinfo) { this.debug('received unknown buffer', { from: formatRinfo(rinfo), buffer: buffer.toString() }); this.emit(Net.EventType.Unknown, buffer, formatRinfo(rinfo)); }; Net.prototype.sendMessages = function sendMessages(messages, host) { var self = this; var bytesAvailable = self.udp.maxDgramSize - Net.MessageTypeSize - Net.LengthSize; var buffers = []; var buffer; var message; var i; for (i = 0; i < messages.length; i++) { message = messages[i]; buffer = new Buffer(Net.MessageTypeSize); Net.WriteMessageType.call(buffer, message.type, 0); buffer = Buffer.concat([buffer, self.swim.codec.encode(message.data)]); if (buffer.length + Net.LengthSize < bytesAvailable) { buffers.push(buffer); bytesAvailable -= buffer.length + Net.LengthSize; } else if (buffers.length === 0) { this.debug('oversized message', { length: buffer.length, message: message }); } else { self.sendBuffer(self.makeCompoundMessage(buffers), host); bytesAvailable = self.udp.maxDgramSize - Net.LengthSize; buffers = []; i--; } } if (buffers.length > 0) { self.sendBuffer(self.makeCompoundMessage(buffers), host); } }; Net.prototype.sendMessage = function sendMessage(message, host) { var header = new Buffer(Net.MessageTypeSize); Net.WriteMessageType.call(header, message.type, 0); if (message.data) { this.piggybackAndSend(Buffer.concat([header, this.swim.codec.encode(message.data)]), host); } else { this.piggybackAndSend(header, host); } }; Net.prototype.piggybackAndSend = function piggybackAndSend(buffer, host) { var bytesAvailable = this.udp.maxDgramSize - Net.MessageTypeSize - Net.LengthSize * 2 - buffer.length; var buffers = this.swim.disseminator.getUpdatesUpTo(bytesAvailable); if (buffers.length === 0) { return this.sendBuffer(buffer, host); } buffers.unshift(buffer); this.sendBuffer(this.makeCompoundMessage(buffers), host); }; Net.prototype.makeCompoundMessage = function makeCompoundMessage(buffers) { var header = new Buffer(Net.MessageTypeSize + Net.LengthSize); Net.WriteMessageType.call(header, MessageType.Compound, 0); Net.WriteLength.call(header, buffers.length, Net.MessageTypeSize); buffers = buffers.map(function toBuffer(buffer) { var header = new Buffer(Net.LengthSize); Net.WriteLength.call(header, buffer.length, 0); return Buffer.concat([header, buffer]); }); buffers.unshift(header); return Buffer.concat(buffers); }; Net.prototype.sendBuffer = function sendBuffer(buffer, host) { var address = host.split(':')[0]; var port = host.split(':')[1]; this.udpSocket.send(buffer, 0, buffer.length, port, address); this.debug('sent buffer', { to: host, length: buffer.length }); }; Net.MessageTypeSize = 1; Net.LengthSize = 2; Net.ReadMessageType = Buffer.prototype.readUInt8; Net.ReadLength = Buffer.prototype.readUInt16LE; Net.WriteMessageType = Buffer.prototype.writeUInt8; Net.WriteLength = Buffer.prototype.writeUInt16LE; Net.Default = { udp: { type: 'udp4', maxDgramSize: 512 } }; Net.EventType = { Error: 'error', Listening: 'listening', Ping: 'ping', PingReq: 'pingreq', Sync: 'sync', Ack: 'ack', Update: 'update', Unknown: 'unknown' }; function formatRinfo(rinfo) { return rinfo.address + ':' + rinfo.port; } module.exports = Net; ================================================ FILE: lib/swim.js ================================================ 'use strict'; var debug = require('debug'); var events = require('events'); var util = require('util'); var Codec = require('./codec'); var Disseminator = require('./disseminator'); var FailureDetector = require('./failure-detector'); var Membership = require('./membership'); var Net = require('./net'); var JoinFailedError = require('./error').JoinFailedError; var InvalidStateError = require('./error').InvalidStateError; var ListenFailedError = require('./error').ListenFailedError; function Swim(opts) { this.opts = opts; this.codec = new Codec({ codec: opts.codec, swim: this, debugIdentifier: opts.local.host }); this.disseminator = new Disseminator({ disseminationFactor: opts.disseminationFactor, swim: this, debugIdentifier: opts.local.host }); this.failureDetector = new FailureDetector({ interval: opts.interval, pingTimeout: opts.pingTimeout, pingReqTimeout: opts.pingReqTimeout, pingReqGroupSize: opts.pingReqGroupSize, swim: this, debugIdentifier: opts.local.host }); this.membership = new Membership({ local: opts.local, suspectTimeout: opts.suspectTimeout, preferCurrentMeta: opts.preferCurrentMeta, swim: this, debugIdentifier: opts.local.host }); this.net = new Net({ udp: { port: parseInt(opts.local.host.split(':')[1]), type: opts.udp && opts.udp.type, maxDgramSize: opts.udp && opts.udp.maxDgramSize }, swim: this, debugIdentifier: opts.local.host }); this.state = Swim.State.Stopped; this.joinTimeout = opts.joinTimeout || Swim.Default.joinTimeout; this.joinCheckInterval = opts.joinCheckInterval || Swim.Default.joinCheckInterval; this.changeListener = this.emit.bind(this, Swim.EventType.Change); this.updateListener = this.emit.bind(this, Swim.EventType.Update); this.debug = debug('swim').bind(undefined, opts.local.host); } util.inherits(Swim, events.EventEmitter); Swim.prototype.bootstrap = function bootstrap(hosts, callback) { var self = this; var err; if (self.state !== Swim.State.Stopped) { err = new InvalidStateError({ current: self.state, expected: Swim.State.Stopped }); if (typeof callback === 'function') { callback(err); } else { self.emit(Swim.EventType.Error, err); } return; } self.net.listen(function onListen(err) { if (err) { err = new ListenFailedError({ host: self.opts.local.host, type: self.opts.udp.type }); if (typeof callback === 'function') { callback(err); } else { self.emit(Swim.EventType.Error, err); } return; } self.failureDetector.start(); self.membership.start(); self.disseminator.start(); self.membership.on(Membership.EventType.Change, self.changeListener); self.membership.on(Membership.EventType.Update, self.updateListener); self.state = Swim.State.Started; self.join(hosts, callback); }); }; Swim.prototype.join = function join(hosts, callback) { var self = this; var err; if (self.state !== Swim.State.Started) { err = new InvalidStateError({ currect: self.state, expected: Swim.State.Started }); if (typeof callback === 'function') { callback(err); } else { self.emit(Swim.EventType.Error, err); } } if (!hosts || hosts.length === 0) { if (typeof callback === 'function') { callback(); } else { self.emit(Swim.EventType.Ready); } return; } hosts = hosts.filter(function notLocal(host) { return host !== self.opts.local.host; }); self.membership.sync(hosts); var checker = null; var timeout = setTimeout(function joinTimeout() { clearInterval(checker); var numberOfHostsResponded = checkJoin(); if (numberOfHostsResponded === 0) { var err = new JoinFailedError({ local: self.localhost(), hosts: hosts, numberOfHostsResponded: numberOfHostsResponded, timeout: self.joinTimeout }); if (typeof callback === 'function') { callback(err); } else { self.emit(Swim.EventType.Error, err); } } }, self.joinTimeout); if (self.joinCheckInterval < self.joinTimeout) { checker = setInterval(checkJoin, self.joinCheckInterval); } function checkJoin() { var numberOfHostsResponded = hosts.reduce(function countRespondedHosts(num, host) { num += self.membership.get(host) ? 1 : 0; return num; }, 0); if (numberOfHostsResponded >= 1) { clearInterval(checker); clearTimeout(timeout); if (typeof callback === 'function') { callback(); } else { self.emit(Swim.EventType.Ready); } } return numberOfHostsResponded; } }; Swim.prototype.leave = function leave() { this.membership.removeListener(Membership.EventType.Update, this.updateListener); this.membership.removeListener(Membership.EventType.Change, this.changeListener); this.disseminator.stop(); this.membership.stop(); this.failureDetector.stop(); this.net.close(); this.state = Swim.State.Stopped; }; Swim.prototype.members = function members(hasLocal, hasFaulty) { return this.membership.all(hasLocal, hasFaulty); }; Swim.prototype.checksum = function checksum() { return this.membership.checksum(); }; Swim.prototype.localhost = function localhost() { return this.membership && this.membership.localhost() || this.opts.local.host; }; Swim.prototype.whoami = function whoami() { return this.localhost(); }; Swim.prototype.updateMeta = function updateMeta(meta) { return this.membership.updateMeta(meta); }; Swim.Default = { joinTimeout: 300, joinCheckInterval: 50 }; Swim.EventType = { Change: 'change', Update: 'update', Error: 'error', Ready: 'ready' }; Swim.State = { Started: 'started', Stopped: 'stopped' }; module.exports = Swim; module.exports.Error = require('./error'); ================================================ FILE: package.json ================================================ { "name": "swim", "version": "0.6.0", "description": "Gossip protocol based on SWIM", "main": "index.js", "scripts": { "cover": "covert test/index -q", "lint": "jshint . --exclude-path=./.gitignore && jscs .", "test": "npm run lint && node test/index | tap-spec" }, "repository": { "type": "git", "url": "https://github.com/mrhooray/swim-js.git" }, "keywords": [ "gossip", "swim", "failure", "detection", "membership" ], "author": "Rui Hu ", "license": "MIT", "bugs": { "url": "https://github.com/mrhooray/swim-js/issues" }, "homepage": "https://github.com/mrhooray/swim-js", "devDependencies": { "async": "^0.9.0", "covert": "^1.0.1", "metrics": "^0.1.9", "jscs": "^1.10.0", "jshint": "^2.6.0", "tap-spec": "^2.2.0", "tape": "^3.4.0" }, "dependencies": { "clone": "^1.0.2", "commander": "^2.6.0", "debug": "^2.1.1", "farmhash": "^2.0.0", "msgpack": "^1.0.2" } } ================================================ FILE: test/codec.js ================================================ 'use strict'; var test = require('tape'); var Codec = require('../lib/codec'); test('Codec constructs a codec instance', function t(assert) { assert.strictEqual(new Codec() instanceof Codec, true); assert.end(); }); test('Codec supports json or msgpack', function t(assert) { assert.doesNotThrow(Codec.bind({}, {codec: 'json'})); assert.doesNotThrow(Codec.bind({}, {codec: 'msgpack'})); assert.throws(Codec.bind({}, {codec: 'protobuf'})); assert.end(); }); test('Codec can decode data encoded by same codec', function t(assert) { var data = {foo: 'bar'}; var jsonCodec = new Codec({codec: 'json'}); var msgpackCodec = new Codec({codec: 'msgpack'}); assert.deepEqual(jsonCodec.decode(jsonCodec.encode(data)), data); assert.deepEqual(msgpackCodec.decode(msgpackCodec.encode(data)), data); assert.end(); }); ================================================ FILE: test/disseminator.js ================================================ 'use strict'; var events = require('events'); var test = require('tape'); var Codec = require('../lib/codec'); var Disseminator = require('../lib/disseminator'); var Membership = require('../lib/membership'); var MessageType = require('../lib/message-type'); var Net = require('../lib/net'); var codec = new Codec(); test('Disseminator honors bytesAvailable, dissemination limit and priority', function t(assert) { var membership = new events.EventEmitter(); var disseminator = new Disseminator({ swim: { codec: codec, membership: membership } }); var minDgramSize = Math.pow(2, 6); var maxDgramSize = Math.pow(2, 9); var numberOfUpdates = Math.ceil(Math.random() * 5); var numberOfMembers = Math.ceil(Math.random() * 3); var numberOfBuffers = 0; var hostToCount = Object.create(null); var buffers; var bytesAvailable; var update; var length; var i; membership.size = function size() { return numberOfMembers; }; disseminator.start(); buffers = disseminator.getUpdatesUpTo(Infinity); assert.strictEqual(buffers.length, 0); for (i = 0; i < numberOfUpdates; i++) { membership.emit(Membership.EventType.Update, { host: 'localhost:' + i }); } bytesAvailable = Math.ceil(Math.random() * (maxDgramSize - minDgramSize)) + minDgramSize; buffers = disseminator.getUpdatesUpTo(bytesAvailable); while (buffers.length > 0) { length = 0; /* jshint loopfunc: true */ buffers.forEach(function verify(buffer) { assert.strictEqual(Net.ReadMessageType.call(buffer, 0), MessageType.Update); length += buffer.length; update = codec.decode(buffer.slice(Net.MessageTypeSize)); Object.keys(hostToCount).forEach(function verifyPriority(host) { assert.strictEqual(hostToCount[update.host] > hostToCount[host], false); }); hostToCount[update.host] = (hostToCount[update.host] || 0) + 1; }); /* jshint loopfunc: false */ assert.strictEqual(length < bytesAvailable, true); numberOfBuffers += buffers.length; bytesAvailable = Math.ceil(Math.random() * (maxDgramSize - minDgramSize)) + minDgramSize; buffers = disseminator.getUpdatesUpTo(bytesAvailable); } assert.strictEqual(numberOfBuffers, numberOfUpdates * Disseminator.Default.disseminationFormula(Disseminator.Default.disseminationFactor, numberOfMembers)); disseminator.stop(); assert.end(); }); test('Disseminator removes over disseminated updates on decrease of dissemination limit', function t(assert) { var membership = new events.EventEmitter(); var disseminator = new Disseminator({ swim: { codec: codec, membership: membership }, disseminationFactor: 1, disseminationFormula: function disseminationFormula(factor, size) { return factor * size; } }); var numberOfMembers = 10; var buffers; membership.size = function size() { return numberOfMembers; }; disseminator.start(); membership.emit(Membership.EventType.Update, { host: 'localhost:22' }); buffers = disseminator.getUpdatesUpTo(Infinity); assert.strictEqual(buffers.length, 1); numberOfMembers = 1; membership.emit(Membership.EventType.Change); buffers = disseminator.getUpdatesUpTo(Infinity); assert.strictEqual(buffers.length, 0); disseminator.stop(); assert.end(); }); ================================================ FILE: test/failure-detector.js ================================================ 'use strict'; var events = require('events'); var test = require('tape'); var FailureDetector = require('../lib/failure-detector'); var Member = require('../lib/member'); var MessageType = require('../lib/message-type'); var Net = require('../lib/net'); var INTERVAL = 100; var MAX_TICKS = 3; var TICKS = Math.ceil(Math.random() * MAX_TICKS); var TICK_OFFSET = -0.5; var MEMBER_A = new Member({ host: 'localhost:1111' }); var MEMBER_B = new Member({ host: 'localhost:2222' }); test('FailureDetector ticks/pings at defined interval', function t(assert) { var pingCalled = 0; var membership = { next: function next() { pingCalled++; } }; var net = new events.EventEmitter(); var failureDetector = new FailureDetector({ swim: { membership: membership, net: net }, interval: INTERVAL }); failureDetector.start(); setTimeout(function verify() { verifyInternal(failureDetector, assert); failureDetector.stop(); assert.strictEqual(pingCalled, TICKS); assert.end(); }, INTERVAL * (TICKS + TICK_OFFSET)); }); test('FailureDetector does not send PingReq when receives Ack of Ping in time', function t(assert) { var pingCalled = 0; var membership = { next: function next() { pingCalled++; return MEMBER_A; }, random: function random() { assert.fail(); } }; var net = new events.EventEmitter(); var failureDetector = new FailureDetector({ swim: { membership: membership, net: net }, interval: INTERVAL }); net.sendMessage = function sendMessage(message, host) { net.emit(Net.EventType.Ack, { seq: message.data.seq }, host); }; failureDetector.start(); setTimeout(function verify() { verifyInternal(failureDetector, assert); failureDetector.stop(); assert.strictEqual(pingCalled, TICKS); assert.end(); }, INTERVAL * (TICKS + TICK_OFFSET)); }); test('FailureDetector sends PingReq when does not receive Ack of Ping in time', function t(assert) { var pingTimeout = 1; var pingReqCalled = 0; var membership = { next: function next() { return MEMBER_A; }, random: function random() { pingReqCalled++; return []; } }; var net = new events.EventEmitter(); var failureDetector = new FailureDetector({ swim: { membership: membership, net: net }, interval: INTERVAL, pingTimeout: pingTimeout }); net.sendMessage = function sendMessage() {}; failureDetector.start(); setTimeout(function verify() { verifyInternal(failureDetector, assert); failureDetector.stop(); assert.strictEqual(pingReqCalled, TICKS); assert.end(); }, INTERVAL * (TICKS + TICK_OFFSET)); }); test('FailureDetector does not emit suspect event when receives Ack of PingReq in time', function t(assert) { var pingTimeout = 1; var pingReqTimeout = 10; var pingReqCalled = 0; var membership = { next: function next() { return MEMBER_A; }, random: function random() { pingReqCalled++; return [MEMBER_B]; } }; var net = new events.EventEmitter(); var failureDetector = new FailureDetector({ swim: { membership: membership, net: net }, interval: INTERVAL, pingTimeout: pingTimeout, pingReqTimeout: pingReqTimeout }); net.sendMessage = function sendMessage(message, host) { if (message.type === MessageType.PingReq) { net.emit(Net.EventType.Ack, { seq: message.data.seq }, host); } }; failureDetector.on(FailureDetector.EventType.Suspect, function onSuspect() { assert.fail(); }); failureDetector.start(); setTimeout(function verify() { verifyInternal(failureDetector, assert); failureDetector.stop(); assert.strictEqual(pingReqCalled, TICKS); assert.end(); }, INTERVAL * (TICKS + TICK_OFFSET)); }); test('FailureDetector emits suspect event when does not receive Ack of PingReq in time', function t(assert) { var pingTimeout = 1; var pingReqTimeout = 1; var suspectEmitted = 0; var membership = { next: function next() { return MEMBER_A; }, random: function random() { return [MEMBER_B]; } }; var net = new events.EventEmitter(); var failureDetector = new FailureDetector({ swim: { membership: membership, net: net }, interval: INTERVAL, pingTimeout: pingTimeout, pingReqTimeout: pingReqTimeout }); net.sendMessage = function sendMessage() {}; failureDetector.on(FailureDetector.EventType.Suspect, function onSuspect(suspect) { suspectEmitted++; assert.strictEqual(suspect, MEMBER_A); }); failureDetector.start(); setTimeout(function verify() { verifyInternal(failureDetector, assert); failureDetector.stop(); assert.strictEqual(suspectEmitted, TICKS); assert.end(); }, INTERVAL * (TICKS + TICK_OFFSET)); }); test('FailureDetector sends Ack when receives Ping', function t(assert) { var membership = { next: function next() {} }; var net = new events.EventEmitter(); var failureDetector = new FailureDetector({ swim: { membership: membership, net: net } }); var seq = Math.random(); net.sendMessage = function sendMessage(message, host) { verifyInternal(failureDetector, assert); failureDetector.stop(); assert.strictEqual(message.type, MessageType.Ack); assert.strictEqual(message.data.seq, seq); assert.strictEqual(host, MEMBER_A.host); assert.end(); }; failureDetector.start(); net.emit(Net.EventType.Ping, { seq: seq }, MEMBER_A.host); }); test('FailureDetector sends Ping to destination when receives PingReq, ' + 'then sends Ack to PingReq requester when receives Ack from destination', function t(assert) { var membership = { next: function next() {} }; var net = new events.EventEmitter(); var failureDetector = new FailureDetector({ swim: { membership: membership, net: net } }); var seq = Math.random(); var sendMessageCalled = 0; net.sendMessage = function sendMessage(message, host) { switch (sendMessageCalled) { case 0: sendMessageCalled++; assert.strictEqual(message.type, MessageType.Ping); assert.strictEqual(host, MEMBER_B.host); net.emit(Net.EventType.Ack, { seq: message.data.seq }, host); break; case 1: verifyInternal(failureDetector, assert); failureDetector.stop(); assert.strictEqual(message.type, MessageType.Ack); assert.strictEqual(host, MEMBER_A.host); assert.end(); break; } }; failureDetector.start(); net.emit(Net.EventType.PingReq, { seq: seq, destination: MEMBER_B.host }, MEMBER_A.host); }); test('FailureDetector does not send Ack to PingReq requester ' + 'when does not receive Ack from destination', function t(assert) { var pingTimeout = 1; var membership = { next: function next() {} }; var net = new events.EventEmitter(); var failureDetector = new FailureDetector({ swim: { membership: membership, net: net }, interval: INTERVAL, pingTimeout: pingTimeout }); var seq = Math.random(); var sendMessageCalled = 0; net.sendMessage = function sendMessage(message, host) { switch (sendMessageCalled) { case 0: sendMessageCalled++; assert.strictEqual(message.type, MessageType.Ping); assert.strictEqual(host, MEMBER_B.host); break; case 1: assert.fail(); break; } }; failureDetector.start(); net.emit(Net.EventType.PingReq, { seq: seq, destination: MEMBER_B.host }, MEMBER_A.host); setTimeout(function verify() { verifyInternal(failureDetector, assert); failureDetector.stop(); assert.end(); }, INTERVAL); }); function verifyInternal(failureDetector, assert) { assert.strictEqual(Object.keys(failureDetector.seqToTimeout).length, 0); assert.strictEqual(Object.keys(failureDetector.seqToCallback).length, 0); } ================================================ FILE: test/index.js ================================================ 'use strict'; require('./codec'); require('./disseminator'); require('./failure-detector'); require('./membership'); require('./net'); ================================================ FILE: test/membership.js ================================================ 'use strict'; var events = require('events'); var test = require('tape'); var FailureDetector = require('../lib/failure-detector'); var Member = require('../lib/member'); var Membership = require('../lib/membership'); var MessageType = require('../lib/message-type'); var Net = require('../lib/net'); var SUSPECT_TIMEOUT = 10; var LOCAL = new Member({ host: 'localhost:0000' }); var MEMBER_A = new Member({ host: 'localhost:1111' }); test('Membership sends suspect update back when receives Ack from member in suspect state', function t(assert) { var failureDetector = new events.EventEmitter(); var net = new events.EventEmitter(); var membership = new Membership({ local: LOCAL.data(), swim: { failureDetector: failureDetector, net: net } }); net.sendMessage = function sendMessage(message, host) { membership.stop(); assert.strictEqual(host, MEMBER_A.host); assert.strictEqual(message.type, MessageType.Update); assert.strictEqual(message.data.state, Member.State.Suspect); assert.deepEqual(message.data, membership.get(MEMBER_A.host).data()); assert.end(); }; membership.start(); failureDetector.emit(FailureDetector.EventType.Suspect, MEMBER_A); net.emit(Net.EventType.Ack, MEMBER_A, MEMBER_A.host); }); test('Membership emits update event when member becomes suspect', function t(assert) { var failureDetector = new events.EventEmitter(); var net = new events.EventEmitter(); var membership = new Membership({ local: LOCAL.data(), swim: { failureDetector: failureDetector, net: net } }); membership.on(Membership.EventType.Update, function onUpdate(data) { membership.stop(); assert.strictEqual(data.state, Member.State.Suspect); assert.deepEqual(data, membership.get(data.host).data()); assert.end(); }); membership.start(); failureDetector.emit(FailureDetector.EventType.Suspect, MEMBER_A); }); test('Membership emits update event when member becomes faulty', function t(assert) { var failureDetector = new events.EventEmitter(); var net = new events.EventEmitter(); var membership = new Membership({ local: LOCAL.data(), swim: { failureDetector: failureDetector, net: net }, suspectTimeout: SUSPECT_TIMEOUT }); var updateEmitted = 0; membership.on(Membership.EventType.Update, function onUpdate(data) { switch (updateEmitted) { case 0: updateEmitted++; assert.strictEqual(data.state, Member.State.Suspect); assert.deepEqual(data, membership.get(data.host).data()); break; case 1: membership.stop(); assert.strictEqual(data.state, Member.State.Faulty); assert.strictEqual(membership.get(data.host), undefined); assert.end(); break; } }); membership.start(); failureDetector.emit(FailureDetector.EventType.Suspect, MEMBER_A); }); test('Membership resumes suspect mechanism after restart', function t(assert) { var failureDetector = new events.EventEmitter(); var net = new events.EventEmitter(); var membership = new Membership({ local: LOCAL.data(), swim: { failureDetector: failureDetector, net: net }, suspectTimeout: SUSPECT_TIMEOUT }); var updateEmitted = 0; membership.on(Membership.EventType.Update, function onUpdate(data) { switch (updateEmitted) { case 0: updateEmitted++; assert.strictEqual(data.state, Member.State.Suspect); assert.deepEqual(data, membership.get(data.host).data()); break; case 1: membership.stop(); assert.strictEqual(data.state, Member.State.Faulty); assert.strictEqual(membership.get(data.host), undefined); assert.end(); break; } }); membership.start(); failureDetector.emit(FailureDetector.EventType.Suspect, MEMBER_A); membership.stop(); membership.start(); }); test('Membership accepts alive update for remote member not in membership', function t(assert) { var failureDetector = new events.EventEmitter(); var net = new events.EventEmitter(); var membership = new Membership({ local: LOCAL.data(), swim: { failureDetector: failureDetector, net: net } }); var update = MEMBER_A.data(); membership.on(Membership.EventType.Update, function onUpdate(data) { membership.stop(); assert.strictEqual(data.state, Member.State.Alive); assert.deepEqual(data, update); assert.deepEqual(data, membership.get(data.host).data()); assert.end(); }); membership.start(); net.emit(Net.EventType.Update, update); }); test('Membership accepts alive update with greater incarnation ' + 'for alive/suspect remote member in membership', function t(assert) { var failureDetector = new events.EventEmitter(); var net = new events.EventEmitter(); var membership = new Membership({ local: LOCAL.data(), swim: { failureDetector: failureDetector, net: net } }); var update = MEMBER_A.data(); var updateEmitted = 0; membership.on(Membership.EventType.Update, function onUpdate(data) { switch (updateEmitted) { case 0: updateEmitted++; assert.strictEqual(data.state, Member.State.Alive); assert.deepEqual(data, update); assert.deepEqual(data, membership.get(data.host).data()); break; case 1: membership.stop(); assert.strictEqual(data.state, Member.State.Alive); assert.deepEqual(data, update); assert.deepEqual(data, membership.get(data.host).data()); assert.end(); break; } }); membership.start(); net.emit(Net.EventType.Update, update); update.incarnation += 1; net.emit(Net.EventType.Update, update); }); test('Membership drops alive update with smaller or equal incarnation ' + 'for alive/suspect remote member in membership', function t(assert) { var failureDetector = new events.EventEmitter(); var net = new events.EventEmitter(); var membership = new Membership({ local: LOCAL.data(), swim: { failureDetector: failureDetector, net: net } }); var update = MEMBER_A.data(); var updateEmitted = 0; var dropEmitted = 0; membership.on(Membership.EventType.Update, function onUpdate(data) { switch (updateEmitted) { case 0: updateEmitted++; assert.strictEqual(data.state, Member.State.Alive); assert.deepEqual(data, update); assert.deepEqual(data, membership.get(data.host).data()); break; case 1: updateEmitted++; assert.strictEqual(data.state, Member.State.Suspect); assert.deepEqual(data, update); assert.deepEqual(data, membership.get(data.host).data()); break; case 2: assert.fail(); break; } }); membership.on(Membership.EventType.Drop, function onDrop(data) { switch (dropEmitted) { case 0: dropEmitted++; assert.strictEqual(updateEmitted, 1); assert.strictEqual(data.state, Member.State.Alive); assert.deepEqual(data, update); break; case 1: dropEmitted++; assert.strictEqual(updateEmitted, 1); assert.strictEqual(data.state, Member.State.Alive); assert.deepEqual(data, update); break; case 2: dropEmitted++; assert.strictEqual(updateEmitted, 2); assert.strictEqual(data.state, Member.State.Alive); assert.deepEqual(data, update); break; case 3: membership.stop(); assert.strictEqual(updateEmitted, 2); assert.strictEqual(data.state, Member.State.Alive); assert.deepEqual(data, update); assert.end(); break; } }); membership.start(); // accept net.emit(Net.EventType.Update, update); // drop net.emit(Net.EventType.Update, update); // drop update.incarnation -= 1; net.emit(Net.EventType.Update, update); // accept update.incarnation += 1; update.state = Member.State.Suspect; net.emit(Net.EventType.Update, update); // drop update.state = Member.State.Alive; net.emit(Net.EventType.Update, update); // drop update.incarnation -= 1; net.emit(Net.EventType.Update, update); }); test('Membership drops alive update with smaller or equal incarnation ' + 'for faulty member', function t(assert) { var failureDetector = new events.EventEmitter(); var net = new events.EventEmitter(); var membership = new Membership({ local: LOCAL.data(), swim: { failureDetector: failureDetector, net: net } }); var update = MEMBER_A.data(); var updateEmitted = 0; var dropEmitted = 0; membership.on(Membership.EventType.Update, function onUpdate(data) { switch (updateEmitted) { case 0: updateEmitted++; assert.strictEqual(data.state, Member.State.Alive); assert.deepEqual(data, update); assert.deepEqual(data, membership.get(data.host).data()); break; case 1: updateEmitted++; assert.strictEqual(data.state, Member.State.Faulty); assert.deepEqual(data, update); assert.deepEqual(membership.get(data.host), undefined); break; case 2: assert.fail(); break; } }); membership.on(Membership.EventType.Drop, function onDrop(data) { switch (dropEmitted) { case 0: dropEmitted++; assert.strictEqual(updateEmitted, 2); assert.strictEqual(data.state, Member.State.Alive); assert.deepEqual(data, update); break; case 1: membership.stop(); assert.strictEqual(updateEmitted, 2); assert.strictEqual(data.state, Member.State.Alive); assert.deepEqual(data, update); assert.end(); break; } }); membership.start(); // accept update.state = Member.State.Alive; net.emit(Net.EventType.Update, update); // accept update.state = Member.State.Faulty; net.emit(Net.EventType.Update, update); // drop update.state = Member.State.Alive; net.emit(Net.EventType.Update, update); // drop update.incarnation -= 1; net.emit(Net.EventType.Update, update); }); test('Membership drops alive update with smaller or equal incarnation for local member', function t(assert) { var failureDetector = new events.EventEmitter(); var net = new events.EventEmitter(); var membership = new Membership({ local: LOCAL.data(), swim: { failureDetector: failureDetector, net: net } }); var update = LOCAL.data(); var dropEmitted = 0; membership.on(Membership.EventType.Update, function onUpdate() { assert.fail(); }); membership.on(Membership.EventType.Drop, function onDrop(data) { switch (dropEmitted) { case 0: dropEmitted++; assert.strictEqual(data.state, Member.State.Alive); assert.deepEqual(data, update); break; case 1: membership.stop(); assert.strictEqual(data.state, Member.State.Alive); assert.deepEqual(data, update); assert.end(); break; } }); membership.start(); net.emit(Net.EventType.Update, update); net.emit(Net.EventType.Update, update); }); test('Membership accepts alive update with greater incarnation for local member ' + 'and local member affirms itself with greater incarnation', function t(assert) { var failureDetector = new events.EventEmitter(); var net = new events.EventEmitter(); var membership = new Membership({ local: LOCAL.data(), swim: { failureDetector: failureDetector, net: net } }); var update = LOCAL.data(); membership.on(Membership.EventType.Update, function onUpdate(data) { membership.stop(); assert.strictEqual(data.state, Member.State.Alive); assert.strictEqual(data.incarnation > update.incarnation, true); assert.end(); }); membership.start(); update.incarnation += 1; net.emit(Net.EventType.Update, update); }); test('Membership accepts suspect update for remote member not in membership', function t(assert) { var failureDetector = new events.EventEmitter(); var net = new events.EventEmitter(); var membership = new Membership({ local: LOCAL.data(), swim: { failureDetector: failureDetector, net: net } }); var update = MEMBER_A.data(); membership.on(Membership.EventType.Update, function onUpdate(data) { membership.stop(); assert.strictEqual(data.state, Member.State.Suspect); assert.deepEqual(data, update); assert.deepEqual(data, membership.get(data.host).data()); assert.end(); }); membership.start(); update.state = Member.State.Suspect; net.emit(Net.EventType.Update, update); }); test('Membership accepts suspect update with greater incarnation ' + 'for alive/suspect remote member in membership', function t(assert) { var failureDetector = new events.EventEmitter(); var net = new events.EventEmitter(); var membership = new Membership({ local: LOCAL.data(), swim: { failureDetector: failureDetector, net: net } }); var update = MEMBER_A.data(); var updateEmitted = 0; membership.on(Membership.EventType.Update, function onUpdate(data) { switch (updateEmitted) { case 0: updateEmitted++; assert.strictEqual(data.state, Member.State.Alive); assert.deepEqual(data, update); assert.deepEqual(data, membership.get(data.host).data()); break; case 1: updateEmitted++; assert.strictEqual(data.state, Member.State.Suspect); assert.deepEqual(data, update); assert.deepEqual(data, membership.get(data.host).data()); break; case 2: membership.stop(); assert.strictEqual(data.state, Member.State.Suspect); assert.deepEqual(data, update); assert.deepEqual(data, membership.get(data.host).data()); assert.end(); break; } }); membership.start(); net.emit(Net.EventType.Update, update); update.incarnation += 1; update.state = Member.State.Suspect; net.emit(Net.EventType.Update, update); update.incarnation += 1; net.emit(Net.EventType.Update, update); }); test('Membership drops suspect update with smaller or equal incarnation ' + 'for suspect remote member in membership', function t(assert) { var failureDetector = new events.EventEmitter(); var net = new events.EventEmitter(); var membership = new Membership({ local: LOCAL.data(), swim: { failureDetector: failureDetector, net: net } }); var update = MEMBER_A.data(); var updateEmitted = 0; var dropEmitted = 0; membership.on(Membership.EventType.Update, function onUpdate(data) { switch (updateEmitted) { case 0: updateEmitted++; assert.strictEqual(data.state, Member.State.Suspect); assert.deepEqual(data, update); assert.deepEqual(data, membership.get(data.host).data()); break; case 1: assert.fail(); break; } }); membership.on(Membership.EventType.Drop, function onDrop(data) { switch (dropEmitted) { case 0: dropEmitted++; assert.strictEqual(updateEmitted, 1); assert.strictEqual(data.state, Member.State.Suspect); assert.deepEqual(data, update); break; case 1: membership.stop(); assert.strictEqual(updateEmitted, 1); assert.strictEqual(data.state, Member.State.Suspect); assert.deepEqual(data, update); assert.end(); break; } }); membership.start(); // accept update.state = Member.State.Suspect; net.emit(Net.EventType.Update, update); // drop net.emit(Net.EventType.Update, update); // drop update.incarnation -= 1; net.emit(Net.EventType.Update, update); }); test('Membership drops suspect update with smaller or equal incarnation ' + 'for faulty member', function t(assert) { var failureDetector = new events.EventEmitter(); var net = new events.EventEmitter(); var membership = new Membership({ local: LOCAL.data(), swim: { failureDetector: failureDetector, net: net } }); var update = MEMBER_A.data(); var updateEmitted = 0; var dropEmitted = 0; membership.on(Membership.EventType.Update, function onUpdate(data) { switch (updateEmitted) { case 0: updateEmitted++; assert.strictEqual(data.state, Member.State.Suspect); assert.deepEqual(data, update); assert.deepEqual(data, membership.get(data.host).data()); break; case 1: updateEmitted++; assert.strictEqual(data.state, Member.State.Faulty); assert.deepEqual(data, update); assert.deepEqual(membership.get(data.host), undefined); break; case 2: assert.fail(); break; } }); membership.on(Membership.EventType.Drop, function onDrop(data) { switch (dropEmitted) { case 0: dropEmitted++; assert.strictEqual(updateEmitted, 2); assert.strictEqual(data.state, Member.State.Suspect); assert.deepEqual(data, update); break; case 1: membership.stop(); assert.strictEqual(updateEmitted, 2); assert.strictEqual(data.state, Member.State.Suspect); assert.deepEqual(data, update); assert.end(); break; } }); membership.start(); // accept update.state = Member.State.Suspect; net.emit(Net.EventType.Update, update); // accept update.state = Member.State.Faulty; net.emit(Net.EventType.Update, update); // drop update.state = Member.State.Suspect; net.emit(Net.EventType.Update, update); // drop update.incarnation -= 1; net.emit(Net.EventType.Update, update); }); test('Membership accepts suspect update with equal incarnation ' + 'for alive remote member in membership', function t(assert) { var failureDetector = new events.EventEmitter(); var net = new events.EventEmitter(); var membership = new Membership({ local: LOCAL.data(), swim: { failureDetector: failureDetector, net: net } }); var update = MEMBER_A.data(); var updateEmitted = 0; membership.on(Membership.EventType.Update, function onUpdate(data) { switch (updateEmitted) { case 0: updateEmitted++; assert.strictEqual(data.state, Member.State.Alive); assert.deepEqual(data, update); assert.deepEqual(data, membership.get(data.host).data()); break; case 1: membership.stop(); assert.strictEqual(data.state, Member.State.Suspect); assert.deepEqual(data, update); assert.deepEqual(data, membership.get(data.host).data()); assert.end(); break; } }); membership.start(); net.emit(Net.EventType.Update, update); update.state = Member.State.Suspect; net.emit(Net.EventType.Update, update); }); test('Membership drops suspect update with smaller incarnation ' + 'for alive remote member in membership', function t(assert) { var failureDetector = new events.EventEmitter(); var net = new events.EventEmitter(); var membership = new Membership({ local: LOCAL.data(), swim: { failureDetector: failureDetector, net: net } }); var update = MEMBER_A.data(); var updateEmitted = 0; membership.on(Membership.EventType.Update, function onUpdate(data) { updateEmitted++; assert.strictEqual(data.state, Member.State.Alive); assert.deepEqual(data, update); assert.deepEqual(data, membership.get(data.host).data()); }); membership.on(Membership.EventType.Drop, function onUpdate(data) { membership.stop(); assert.strictEqual(updateEmitted, 1); assert.strictEqual(data.state, Member.State.Suspect); assert.deepEqual(data, update); assert.end(); }); membership.start(); net.emit(Net.EventType.Update, update); update.incarnation -= 1; update.state = Member.State.Suspect; net.emit(Net.EventType.Update, update); }); test('Membership drops suspect update with for local member ' + 'and local member affirms itself with greater incarnation', function t(assert) { var failureDetector = new events.EventEmitter(); var net = new events.EventEmitter(); var membership = new Membership({ local: LOCAL.data(), swim: { failureDetector: failureDetector, net: net } }); var update = LOCAL.data(); var dropEmitted = 0; membership.on(Membership.EventType.Update, function onUpdate(data) { membership.stop(); assert.strictEqual(dropEmitted, 1); assert.strictEqual(data.state, Member.State.Alive); assert.strictEqual(data.incarnation > update.incarnation, true); assert.end(); }); membership.on(Membership.EventType.Drop, function onDrop(data) { dropEmitted++; assert.strictEqual(data.state, Member.State.Suspect); assert.deepEqual(data, update); }); membership.start(); update.state = Member.State.Suspect; net.emit(Net.EventType.Update, update); }); test('Membership accepts faulty update with greater or equal incarnation ' + 'for remote member in membership', function t(assert) { var failureDetector = new events.EventEmitter(); var net = new events.EventEmitter(); var membership = new Membership({ local: LOCAL.data(), swim: { failureDetector: failureDetector, net: net } }); var update = MEMBER_A.data(); var updateEmitted = 0; membership.on(Membership.EventType.Update, function onUpdate(data) { switch (updateEmitted) { case 0: updateEmitted++; assert.strictEqual(data.state, Member.State.Alive); assert.deepEqual(data, update); assert.deepEqual(data, membership.get(data.host).data()); break; case 1: updateEmitted++; assert.strictEqual(data.state, Member.State.Faulty); assert.deepEqual(data, update); assert.strictEqual(membership.get(data.host), undefined); break; case 2: updateEmitted++; assert.strictEqual(data.state, Member.State.Alive); assert.deepEqual(data, update); assert.deepEqual(data, membership.get(data.host).data()); break; case 3: membership.stop(); assert.strictEqual(data.state, Member.State.Faulty); assert.deepEqual(data, update); assert.strictEqual(membership.get(data.host), undefined); assert.end(); break; } }); membership.start(); net.emit(Net.EventType.Update, update); update.state = Member.State.Faulty; net.emit(Net.EventType.Update, update); update.incarnation += 1; update.state = Member.State.Alive; net.emit(Net.EventType.Update, update); update.incarnation += 1; update.state = Member.State.Faulty; net.emit(Net.EventType.Update, update); }); test('Membership drops faulty update with for remote member not in membership', function t(assert) { var failureDetector = new events.EventEmitter(); var net = new events.EventEmitter(); var membership = new Membership({ local: LOCAL.data(), swim: { failureDetector: failureDetector, net: net } }); var update = MEMBER_A.data(); membership.on(Membership.EventType.Drop, function onDrop(data) { membership.stop(); assert.strictEqual(data.state, Member.State.Faulty); assert.deepEqual(data, update); assert.end(); }); membership.start(); update.state = Member.State.Faulty; net.emit(Net.EventType.Update, update); }); test('Membership drops faulty update with smaller incarnation for remote member in membership', function t(assert) { var failureDetector = new events.EventEmitter(); var net = new events.EventEmitter(); var membership = new Membership({ local: LOCAL.data(), swim: { failureDetector: failureDetector, net: net } }); var update = MEMBER_A.data(); var updateEmitted = 0; membership.on(Membership.EventType.Update, function onUpdate(data) { updateEmitted++; assert.strictEqual(data.state, Member.State.Alive); assert.deepEqual(data, update); assert.deepEqual(data, membership.get(data.host).data()); }); membership.on(Membership.EventType.Drop, function onDrop(data) { membership.stop(); assert.strictEqual(updateEmitted, 1); assert.strictEqual(data.state, Member.State.Faulty); assert.deepEqual(data, update); assert.end(); }); membership.start(); net.emit(Net.EventType.Update, update); update.incarnation -= 1; update.state = Member.State.Faulty; net.emit(Net.EventType.Update, update); }); test('Membership drops faulty update with for local member ' + 'and local member affirms itself with greater incarnation', function t(assert) { var failureDetector = new events.EventEmitter(); var net = new events.EventEmitter(); var membership = new Membership({ local: LOCAL.data(), swim: { failureDetector: failureDetector, net: net } }); var update = LOCAL.data(); var dropEmitted = 0; membership.on(Membership.EventType.Update, function onUpdate(data) { membership.stop(); assert.strictEqual(dropEmitted, 1); assert.strictEqual(data.state, Member.State.Alive); assert.strictEqual(data.incarnation > update.incarnation, true); assert.end(); }); membership.on(Membership.EventType.Drop, function onDrop(data) { assert.strictEqual(dropEmitted, 0); assert.strictEqual(data.state, Member.State.Faulty); assert.deepEqual(data, update); dropEmitted++; }); membership.start(); update.state = Member.State.Faulty; net.emit(Net.EventType.Update, update); }); test('Membership is a infinite round robin iterator', function t(assert) { var failureDetector = new events.EventEmitter(); var net = new events.EventEmitter(); var membership = new Membership({ local: LOCAL.data(), swim: { failureDetector: failureDetector, net: net } }); var numberOfMembers = Math.ceil(Math.random() * 10); var numberOfIterations = Math.ceil(Math.random() * 5); var hostToCount = Object.create(null); var member; var i; var j; membership.start(); for (i = 0; i < numberOfMembers; i++) { net.emit(Net.EventType.Update, new Member({ host: 'localhost:' + i }).data()); } membership.stop(); for (i = 0; i < numberOfMembers; i++) { member = membership.next(); hostToCount[member.host] = (hostToCount[member.host] || 0) + 1; } Object.keys(hostToCount).forEach(function verify(host) { assert.strictEqual(hostToCount[host], 1); }); for (j = 0; j < numberOfIterations; j++) { for (i = 0; i < numberOfMembers; i++) { member = membership.next(); hostToCount[member.host] = (hostToCount[member.host] || 0) + 1; } } Object.keys(hostToCount).forEach(function verify(host) { assert.strictEqual(hostToCount[host], numberOfIterations + 1); }); assert.strictEqual(hostToCount[LOCAL.host], undefined); assert.end(); }); test('Membership next to undefined when there is only local member', function t(assert) { var failureDetector = new events.EventEmitter(); var net = new events.EventEmitter(); var membership = new Membership({ local: LOCAL.data(), swim: { failureDetector: failureDetector, net: net } }); assert.strictEqual(membership.next(), undefined); assert.end(); }); test('Membership can randomly select up to n unique remote members', function t(assert) { var failureDetector = new events.EventEmitter(); var net = new events.EventEmitter(); var membership = new Membership({ local: LOCAL.data(), swim: { failureDetector: failureDetector, net: net } }); var numberOfMembers = Math.ceil(Math.random() * 10); var numberOfIterations = Math.ceil(Math.random() * 5); var selected; var n; var i; var j; assert.strictEqual(membership.random(1).length, 0); membership.start(); for (i = 0; i < numberOfMembers; i++) { net.emit(Net.EventType.Update, new Member({ host: 'localhost:' + i }).data()); } membership.stop(); for (j = 0; j < numberOfIterations; j++) { n = Math.floor(Math.random() * numberOfMembers * 2); selected = membership.random(n); assert.strictEqual(Array.isArray(selected), true); assert.strictEqual(selected.length <= n, true); /* jshint loopfunc: true */ assert.strictEqual(Object.keys(selected.reduce(function mark(result, curr) { result[curr.host] = true; return result; }, Object.create(null))).length, selected.length); /* jshint loopfunc: false */ } assert.end(); }); ================================================ FILE: test/net.js ================================================ 'use strict'; var async = require('async'); var test = require('tape'); var Codec = require('../lib/codec'); var MessageType = require('../lib/message-type'); var Net = require('../lib/net'); var codec = new Codec(); test('Net can send and receive primitive messages', function t(assert) { var senderOpts = { swim: { codec: codec, disseminator: { getUpdatesUpTo: function getUpdatesUpTo() { return []; } } }, udp: { port: 0 } }; var receiverOpts = { swim: { codec: codec }, udp: { port: 0 } }; var sender = new Net(senderOpts); var receiver = new Net(receiverOpts); var tests = [{ type: MessageType.Ping, data: { seq: Math.random() }, event: Net.EventType.Ping }, { type: MessageType.PingReq, data: { seq: Math.random() }, event: Net.EventType.PingReq }, { type: MessageType.Sync, data: { seq: Math.random() }, event: Net.EventType.Sync }, { type: MessageType.Ack, data: { seq: Math.random() }, event: Net.EventType.Ack }, { type: MessageType.Update, data: { seq: Math.random() }, event: Net.EventType.Update }]; var senderPort; var receiverPort; async.parallel([ sender.listen.bind(sender), receiver.listen.bind(receiver) ], function parallelCallback(err) { assert.notOk(err); senderPort = sender.udpSocket.address().port; receiverPort = receiver.udpSocket.address().port; async.each(tests, function runTest(test, callback) { receiver.on(test.event, function onEvent(data, host) { assert.deepEqual(data, test.data); assert.strictEqual(parseInt(host.split(':')[1]), senderPort); receiver.removeAllListeners(test.event); callback(); }); sender.sendMessage({ type: test.type, data: test.data }, 'localhost:' + receiverPort); }, function eachCallback() { sender.close(); receiver.close(); assert.end(); }); }); }); test('Net can send and receive primitive messages with piggybacked updates', function t(assert) { var randomPingData = { seq: Math.random() }; var randomUpdateData = { seq: Math.random() }; var senderOpts = { swim: { codec: codec, disseminator: { getUpdatesUpTo: function getUpdatesUpTo(bytesAvailable) { var expectedBytesAvailable = Net.Default.udp.maxDgramSize - Net.MessageTypeSize - Net.LengthSize * 2 - Net.MessageTypeSize - codec.encode(randomPingData).length; var header; assert.strictEqual(bytesAvailable, expectedBytesAvailable); header = new Buffer(Net.MessageTypeSize); Net.WriteMessageType.call(header, MessageType.Update, 0); return [Buffer.concat([header, codec.encode(randomUpdateData)])]; } } }, udp: { port: 0 } }; var receiverOpts = { swim: { codec: codec }, udp: { port: 0 } }; var sender = new Net(senderOpts); var receiver = new Net(receiverOpts); var senderPort; var receiverPort; async.parallel([ sender.listen.bind(sender), receiver.listen.bind(receiver) ], function parallelCallback(err) { assert.notOk(err); senderPort = sender.udpSocket.address().port; receiverPort = receiver.udpSocket.address().port; async.parallel([ function receivePing(callback) { receiver.on(Net.EventType.Ping, function onPing(data, host) { assert.deepEqual(data, randomPingData); assert.strictEqual(parseInt(host.split(':')[1]), senderPort); callback(); }); }, function receiveUpdate(callback) { receiver.on(Net.EventType.Update, function onUpdate(data, host) { assert.deepEqual(data, randomUpdateData); assert.strictEqual(parseInt(host.split(':')[1]), senderPort); callback(); }); }, function send(callback) { sender.sendMessage({ type: MessageType.Ping, data: randomPingData }, 'localhost:' + receiverPort); callback(); } ], function parallelCallback() { receiver.removeAllListeners(); sender.close(); receiver.close(); assert.end(); }); }); }); test('Net can send and receive multiple messages batched over packets', function t(assert) { var senderOpts = { swim: { codec: codec }, udp: { port: 0 } }; var receiverOpts = { swim: { codec: codec }, udp: { port: 0 } }; var sender = new Net(senderOpts); var receiver = new Net(receiverOpts); var messages = []; var received = []; var messageCount = 100; var packetCount = 0; var senderPort; var receiverPort; var i; for (i = 0; i < messageCount; i++) { messages.push({ type: MessageType.Update, data: { seq: Math.random() } }); } sender.sendBuffer = function sendBuffer(buffer, host) { packetCount++; Net.prototype.sendBuffer.call(sender, buffer, host); }; async.parallel([ sender.listen.bind(sender), receiver.listen.bind(receiver) ], function parallelCallback(err) { assert.notOk(err); senderPort = sender.udpSocket.address().port; receiverPort = receiver.udpSocket.address().port; async.parallel([ function receiveUpdate(callback) { receiver.on(Net.EventType.Update, function onUpdate(data) { received.push({ type: MessageType.Update, data: data }); if (received.length === messages.length) { callback(); } }); }, function send(callback) { sender.sendMessages(messages, 'localhost:' + receiverPort); callback(); } ], function parallelCallback() { assert.strictEqual(packetCount < messageCount, true); assert.deepEqual(received.length, messages.length); assert.deepEqual(sum(received), sum(messages)); receiver.removeAllListeners(); sender.close(); receiver.close(); assert.end(); }); }); }); function sum(messages) { messages.reduce(function sum(acc, m) { return acc += m.seq; }, 0); }