From 02d33112060eb3205f846bb6bba171f37dcd066f Mon Sep 17 00:00:00 2001 From: Jeff Wolski Date: Mon, 30 Mar 2015 17:10:21 -0700 Subject: [PATCH] Significant refactor of membership change paths in prep for smaller damping pull request --- index.js | 100 ++-- lib/dissemination.js | 131 +++++ lib/member.js | 8 +- lib/members.js | 473 ------------------ .../membership-iterator.js | 49 +- ..._rollup.js => membership-update-rollup.js} | 0 lib/membership-update-rules.js | 68 +++ lib/membership.js | 274 ++++++++++ lib/swim/ping-req-recvr.js | 2 +- lib/swim/ping-req-sender.js | 5 +- lib/swim/ping-sender.js | 2 +- lib/swim/suspicion.js | 4 +- test/index_test.js | 64 +-- test/integration/join-test.js | 2 +- test/lib/test-ringpop.js | 4 +- test/member_iterator_test.js | 88 ---- test/members_test.js | 84 ---- test/membership-iterator-test.js | 71 +++ test/membership-test.js | 140 ++++++ ...st.js => membership-update-rollup-test.js} | 2 +- 20 files changed, 791 insertions(+), 780 deletions(-) create mode 100644 lib/dissemination.js delete mode 100644 lib/members.js rename test/local-incarnation-test.js => lib/membership-iterator.js (57%) rename lib/{membership_update_rollup.js => membership-update-rollup.js} (100%) create mode 100644 lib/membership-update-rules.js create mode 100644 lib/membership.js delete mode 100644 test/member_iterator_test.js delete mode 100644 test/members_test.js create mode 100644 test/membership-iterator-test.js create mode 100644 test/membership-test.js rename test/{membership_update_rollup_test.js => membership-update-rollup-test.js} (98%) diff --git a/index.js b/index.js index b6790027..087927e8 100644 --- a/index.js +++ b/index.js @@ -32,13 +32,13 @@ var sendPingReq = require('./lib/swim/ping-req-sender.js'); var Suspicion = require('./lib/swim/suspicion'); var createRingPopTChannel = require('./lib/tchannel.js').createRingPopTChannel; -var Dissemination = require('./lib/members').Dissemination; +var Dissemination = require('./lib/dissemination.js'); var errors = require('./lib/errors.js'); var HashRing = require('./lib/ring'); var joinCluster = require('./lib/join_cluster.js').joinCluster; -var MemberIterator = require('./lib/members').MemberIterator; -var Membership = require('./lib/members').Membership; -var MembershipUpdateRollup = require('./lib/membership_update_rollup.js'); +var Membership = require('./lib/membership.js'); +var MembershipIterator = require('./lib/membership-iterator.js'); +var MembershipUpdateRollup = require('./lib/membership-update-rollup.js'); var nulls = require('./lib/nulls'); var rawHead = require('./lib/request-proxy/util.js').rawHead; var RequestProxy = require('./lib/request-proxy/index.js'); @@ -128,7 +128,7 @@ function RingPop(options) { this.dissemination = new Dissemination(this); this.membership = new Membership(this); this.membership.on('updated', this.onMembershipUpdated.bind(this)); - this.memberIterator = new MemberIterator(this); + this.memberIterator = new MembershipIterator(this); this.gossip = new Gossip({ ringpop: this, minProtocolPeriod: options.minProtocolPeriod @@ -188,14 +188,6 @@ RingPop.prototype.setupChannel = function setupChannel() { createRingPopTChannel(this, this.channel); }; -RingPop.prototype.addLocalMember = function addLocalMember(info) { - this.membership.addMember({ - address: this.hostPort, - incarnationNumber: info && info.incarnationNumber, - status: 'alive' - }); -}; - RingPop.prototype.adminJoin = function adminJoin(callback) { if (!this.membership.localMember) { process.nextTick(function() { @@ -239,7 +231,9 @@ RingPop.prototype.adminLeave = function adminLeave(callback) { } // TODO Explicitly infect other members (like admin join)? - this.membership.makeLeave(); + this.membership.makeLeave(this.whoami(), + this.membership.localMember.incarnationNumber); + this.gossip.stop(); this.suspicion.stopAll(); @@ -282,7 +276,8 @@ RingPop.prototype.bootstrap = function bootstrap(opts, callback) { this.checkForMissingBootstrapHost(); this.checkForHostnameIpMismatch(); - this.addLocalMember(); + // Add local member to membership. + this.membership.makeAlive(this.whoami(), Date.now()); this.adminJoin(function(err, nodesJoined) { if (err) { @@ -441,12 +436,12 @@ RingPop.prototype.protocolJoin = function protocolJoin(options, callback) { this.serverRate.mark(); this.totalRate.mark(); - this.membership.makeJoin(joinerAddress, options.incarnationNumber); + this.membership.makeAlive(joinerAddress, options.incarnationNumber); callback(null, { app: this.app, coordinator: this.whoami(), - membership: this.membership.getState() + membership: this.dissemination.fullSync() }); }; @@ -467,7 +462,7 @@ RingPop.prototype.protocolPing = function protocolPing(options, callback) { this.membership.update(changes); callback(null, { - changes: this.issueMembershipChanges(checksum, source) + changes: this.dissemination.issueChanges(checksum, source) }); }; @@ -495,79 +490,51 @@ RingPop.prototype.whoami = function whoami() { return this.hostPort; }; -RingPop.prototype.issueMembershipChanges = function issueMembershipChanges(checksum, source) { - return this.dissemination.getChanges(checksum, source); -}; - -RingPop.prototype.onMemberAlive = function onMemberAlive(member) { +RingPop.prototype.onMemberAlive = function onMemberAlive(change) { this.stat('increment', 'membership-update.alive'); this.logger.debug('member is alive', { local: this.membership.localMember.address, - alive: member.address - }); - - this.dissemination.addChange({ - address: member.address, - status: member.status, - incarnationNumber: member.incarnationNumber, - piggybackCount: 0 + alive: change.address }); - this.ring.addServer(member.address); - this.suspicion.stop(member); + this.dissemination.recordChange(change); + this.ring.addServer(change.address); + this.suspicion.stop(change); }; -RingPop.prototype.onMemberFaulty = function onMemberFaulty(member) { +RingPop.prototype.onMemberFaulty = function onMemberFaulty(change) { this.stat('increment', 'membership-update.faulty'); this.logger.debug('member is faulty', { local: this.membership.localMember.address, - faulty: member.address - }); - - this.dissemination.addChange({ - address: member.address, - status: member.status, - incarnationNumber: member.incarnationNumber, - piggybackCount: 0 + faulty: change.address, }); - this.ring.removeServer(member.address); - this.suspicion.stop(member); + this.dissemination.recordChange(change); + this.ring.removeServer(change.address); + this.suspicion.stop(change); }; -RingPop.prototype.onMemberLeave = function onMemberLeave(member) { +RingPop.prototype.onMemberLeave = function onMemberLeave(change) { this.stat('increment', 'membership-update.leave'); this.logger.debug('member has left', { local: this.membership.localMember.address, - left: member.address + left: change.address }); - this.dissemination.addChange({ - address: member.address, - status: member.status, - incarnationNumber: member.incarnationNumber, - piggybackCount: 0 - }); - - this.ring.removeServer(member.address); - this.suspicion.stop(member); + this.dissemination.recordChange(change); + this.ring.removeServer(change.address); + this.suspicion.stop(change); }; -RingPop.prototype.onMemberSuspect = function onMemberSuspect(member) { +RingPop.prototype.onMemberSuspect = function onMemberSuspect(change) { this.stat('increment', 'membership-update.suspect'); this.logger.debug('member is suspect', { local: this.membership.localMember.address, - suspect: member.address + suspect: change.address }); - this.suspicion.start(member); - - this.dissemination.addChange({ - address: member.address, - status: member.status, - incarnationNumber: member.incarnationNumber, - piggybackCount: 0 - }); + this.suspicion.start(change); + this.dissemination.recordChange(change); }; RingPop.prototype.onMembershipUpdated = function onMembershipUpdated(updates) { @@ -683,7 +650,8 @@ RingPop.prototype.readHostsFile = function readHostsFile(file) { }; RingPop.prototype.rejoin = function rejoin(callback) { - this.membership.affirmAliveness(); + // Assert local member is alive. + this.membership.makeAlive(this.whoami(), Date.now()); this.gossip.start(); this.suspicion.reenable(); diff --git a/lib/dissemination.js b/lib/dissemination.js new file mode 100644 index 00000000..d95d2432 --- /dev/null +++ b/lib/dissemination.js @@ -0,0 +1,131 @@ +// Copyright (c) 2015 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +'use strict'; + +var MembershipUpdateRules = require('./membership-update-rules.js'); + +var LOG_10 = Math.log(10); + +function Dissemination(ringpop) { + this.ringpop = ringpop; + this.ringpop.on('changed', this.onRingChanged.bind(this)); + + this.changes = {}; + this.maxPiggybackCount = 1; + this.piggybackFactor = 15; // A lower piggyback factor leads to more full-syncs +} + +Dissemination.prototype.adjustMaxPiggybackCount = function adjustMaxPiggybackCount() { + var serverCount = this.ringpop.ring.getServerCount(); + var prevPiggybackCount = this.maxPiggybackCount; + var newPiggybackCount = this.piggybackFactor * Math.ceil(Math.log(serverCount + 1) / LOG_10); + + if (this.maxPiggybackCount !== newPiggybackCount) { + this.maxPiggybackCount = newPiggybackCount; + this.ringpop.stat('gauge', 'max-piggyback', this.maxPiggybackCount); + this.ringpop.logger.debug('adjusted max piggyback count', { + newPiggybackCount: this.maxPiggybackCount, + oldPiggybackCount: prevPiggybackCount, + piggybackFactor: this.piggybackFactor, + serverCount: serverCount + }); + } +}; + +Dissemination.prototype.fullSync = function fullSync() { + var changes = []; + + for (var i = 0; i < this.ringpop.membership.members; i++) { + var member = this.ringpop.membership.members[i]; + + changes.push({ + source: this.ringpop.whoami(), + address: member.address, + status: member.status, + incarnationNumber: member.incarnationNumber + }); + } + + return changes; +}; + +Dissemination.prototype.issueChanges = function issueChanges(checksum, source) { + var changesToDisseminate = []; + + var changedNodes = Object.keys(this.changes); + + for (var i = 0; i < changedNodes.length; i++) { + var address = changedNodes[i]; + var change = this.changes[address]; + + // TODO We're bumping the piggyback count even though + // we don't know whether the change successfully made + // it over to the other side. This can result in undesired + // full-syncs. + if (typeof change.piggybackCount === 'undefined') { + change.piggybackCount = 0; + } + + change.piggybackCount += 1; + + if (change.piggybackCount > this.maxPiggybackCount) { + delete this.changes[address]; + continue; + } + + // TODO Compute change ID + // TODO Include change timestamp + changesToDisseminate.push({ + source: change.source, + address: change.address, + status: change.status, + incarnationNumber: change.incarnationNumber + }); + } + + this.ringpop.stat('gauge', 'changes.disseminate', changesToDisseminate.length); + + if (changesToDisseminate.length) { + return changesToDisseminate; + } else if (checksum && this.ringpop.membership.checksum !== checksum) { + this.ringpop.stat('increment', 'full-sync'); + this.ringpop.logger.info('full sync', { + localChecksum: this.ringpop.membership.checksum, + remoteChecksum: checksum, + remoteNode: source + }); + + // TODO Somehow send back indication of isFullSync + return this.fullSync(); + } else { + return []; + } +}; + +Dissemination.prototype.onRingChanged = function onRingChanged() { + this.adjustMaxPiggybackCount(); +}; + +Dissemination.prototype.recordChange = function recordChange(change) { + this.changes[change.address] = change; +}; + +module.exports = Dissemination; diff --git a/lib/member.js b/lib/member.js index fd19e658..f763d61b 100644 --- a/lib/member.js +++ b/lib/member.js @@ -24,10 +24,10 @@ function Member() { } Member.Status = { - Alive: 'alive', - Faulty: 'faulty', - Leave: 'leave', - Suspect: 'suspect' + alive: 'alive', + faulty: 'faulty', + leave: 'leave', + suspect: 'suspect' }; module.exports = Member; diff --git a/lib/members.js b/lib/members.js deleted file mode 100644 index 6fd24382..00000000 --- a/lib/members.js +++ /dev/null @@ -1,473 +0,0 @@ -// Copyright (c) 2015 Uber Technologies, Inc. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. -var _ = require('underscore'); -var EventEmitter = require('events').EventEmitter; -var farmhash = require('farmhash'); -var Member = require('./member.js'); -var util = require('util'); - -var LOG_10 = Math.log(10); -var MEMBER_STATUSES = { - alive: true, - faulty: true, - leave: true, - suspect: true -}; - -function Dissemination(ringpop) { - this.ringpop = ringpop; - this.ringpop.on('changed', this.onRingChanged.bind(this)); - - this.changes = {}; - this.maxPiggybackCount = 1; - this.piggybackFactor = 15; // A lower piggyback factor leads to more full-syncs -} - -Dissemination.prototype.addChange = function addChange(change) { - this.changes[change.address] = change; -}; - -Dissemination.prototype.adjustMaxPiggybackCount = function adjustMaxPiggybackCount() { - var serverCount = this.ringpop.ring.getServerCount(); - var prevPiggybackCount = this.maxPiggybackCount; - var newPiggybackCount = this.piggybackFactor * Math.ceil(Math.log(serverCount + 1) / LOG_10); - - if (this.maxPiggybackCount !== newPiggybackCount) { - this.maxPiggybackCount = newPiggybackCount; - this.ringpop.stat('gauge', 'max-piggyback', this.maxPiggybackCount); - this.ringpop.logger.debug('adjusted max piggyback count', { - newPiggybackCount: this.maxPiggybackCount, - oldPiggybackCount: prevPiggybackCount, - piggybackFactor: this.piggybackFactor, - serverCount: serverCount - }); - } -}; - -Dissemination.prototype.getChanges = function getChanges(checksum, source) { - var changesToDisseminate = []; - - var changedNodes = Object.keys(this.changes); - - for (var i = 0; i < changedNodes.length; i++) { - var address = changedNodes[i]; - var change = this.changes[address]; - - // TODO We're bumping the piggyback count even though - // we don't know whether the change successfully made - // it over to the other side. This can result in undesired - // full-syncs. - change.piggybackCount += 1; - - if (change.piggybackCount > this.maxPiggybackCount) { - delete this.changes[address]; - continue; - } - - changesToDisseminate.push({ - address: change.address, - status: change.status, - incarnationNumber: change.incarnationNumber - }); - } - - this.ringpop.stat('gauge', 'changes.disseminate', changesToDisseminate.length); - - if (changesToDisseminate.length) { - return changesToDisseminate; - } else if (checksum && this.ringpop.membership.checksum !== checksum) { - this.ringpop.stat('increment', 'full-sync'); - this.ringpop.logger.info('full sync', { - localChecksum: this.ringpop.membership.checksum, - remoteChecksum: checksum, - remoteNode: source - }); - - return this.ringpop.membership.getState(); - } else { - return []; - } -}; - -Dissemination.prototype.onRingChanged = function onRingChanged() { - this.adjustMaxPiggybackCount(); -}; - -function MemberIterator(ring) { - this.ring = ring; - this.currentIndex = -1; - this.currentRound = 0; -} - -MemberIterator.prototype.next = function next() { - var membersVisited = {}; - var maxMembersToVisit = this.ring.membership.getMemberCount(); - - while (Object.keys(membersVisited).length < maxMembersToVisit) { - this.currentIndex++; - - if (this.currentIndex >= this.ring.membership.getMemberCount()) { - this.currentIndex = 0; - this.currentRound++; - this.ring.membership.shuffle(); - } - - var member = this.ring.membership.getMemberAt(this.currentIndex); - - membersVisited[member.address] = true; - - if (Membership.isPingable(member)) { - return member; - } - } - - return null; -}; - -function Membership(ringpop) { - this.ringpop = ringpop; - this.members = []; - this.version = 0; - this.checksum = null; -} - -util.inherits(Membership, EventEmitter); - -Membership.evalOverride = function evalOverride(member, change) { - if (Membership.isLocalSuspectOverride(member, change) || Membership.isLocalFaultyOverride(member, change)) { - // Local node should never allow itself to become suspect or faulty. In response, - // it affirms its "aliveness" and bumps its incarnation number. - member.status = 'alive'; - member.incarnationNumber = +new Date(); - return member; - } else if (Membership.isAliveOverride(member, change)) { - member.status = 'alive'; - member.incarnationNumber = change.incarnationNumber || member.incarnationNumber; - return member; - } else if (Membership.isSuspectOverride(member, change)) { - member.status = 'suspect'; - member.incarnationNumber = change.incarnationNumber || member.incarnationNumber; - return member; - } else if (Membership.isFaultyOverride(member, change)) { - member.status = 'faulty'; - member.incarnationNumber = change.incarnationNumber || member.incarnationNumber; - return member; - } else if (Membership.isLeaveOverride(member, change)) { - member.status = 'leave'; - member.incarnationNumber = change.incarnationNumber || member.incarnationNumber; - return member; - } -}; - -Membership.isAliveOverride = function isAliveOverride(member, change) { - return change.status === 'alive' && - MEMBER_STATUSES[member.status] && - change.incarnationNumber > member.incarnationNumber; -}; - -Membership.isFaultyOverride = function isFaultyOverride(member, change) { - return change.status === 'faulty' && - ((member.status === 'suspect' && change.incarnationNumber >= member.incarnationNumber) || - (member.status === 'faulty' && change.incarnationNumber > member.incarnationNumber) || - (member.status === 'alive' && change.incarnationNumber >= member.incarnationNumber)); -}; - -Membership.isLeaveOverride = function isLeaveOverride(member, change) { - return change.status === 'leave' && - MEMBER_STATUSES[member.status] && - change.incarnationNumber > member.incarnationNumber; -}; - -Membership.isLocalFaultyOverride = function isLocalFaultyOverride(member, change) { - return member.isLocal && - change.status === Member.Status.Faulty; -}; - -Membership.isLocalSuspectOverride = function isLocalSuspectOverride(member, change) { - return member.isLocal && - change.status === Member.Status.Suspect; -}; - -Membership.isSuspectOverride = function isSuspectOverride(member, change) { - return change.status === 'suspect' && - ((member.status === 'suspect' && change.incarnationNumber > member.incarnationNumber) || - (member.status === 'faulty' && change.incarnationNumber > member.incarnationNumber) || - (member.status === 'alive' && change.incarnationNumber >= member.incarnationNumber)); -}; - -Membership.isPingable = function isPingable(member) { - return !member.isLocal && member.status === 'alive' || member.status === 'suspect'; -}; - -Membership.prototype.addMember = function addMember(member, force, noEvent) { - if (!force && this.hasMember(member)) { - return member; - } - - var newMember = { - address: member.address, - status: member.status, - incarnationNumber: member.incarnationNumber || +new Date(), - isLocal: this.ringpop.hostPort === member.address - }; - - if (newMember.isLocal) { - this.localMember = newMember; - } - - this.members.splice(this.getJoinPosition(), 0, newMember); - - if (!noEvent) { - this._emitUpdated(newMember); - } - - return newMember; -}; - -Membership.prototype.affirmAliveness = function affirmAliveness() { - this.update([{ - address: this.localMember.address, - status: 'alive', - incarnationNumber: +new Date() - }]); -}; - -Membership.prototype.computeChecksum = function computeChecksum() { - /* The membership checksum is a farmhash of the checksum string computed - * for each member then joined with all other member checksum strings by ';'. - * As an example, the checksum string for a member might be: - * - * localhost:3000alive1414142122274 - * - * And joined together with other members: - * - * localhost:3000alive1414142122274;localhost:3001alive1414142122275 - * - * The member fields that are part of the checksum string are: address, status and - * incarnation number. - */ - var start = new Date(); - - this.checksum = farmhash.hash32(this.generateChecksumString()); - - this.ringpop.stat('timing', 'compute-checksum', start); - this.ringpop.stat('gauge', 'checksum', this.checksum); - - return this.checksum; -}; - -Membership.prototype.findMemberByAddress = function findMemberByAddress(address) { - for (var i = 0; i < this.members.length; i++) { - var member = this.members[i]; - - if (member.address === address) { - return member; - } - } - - return null; -}; - -Membership.prototype.generateChecksumString = function generateChecksumString() { - var checksumStrings = []; - - for (var i = 0; i < this.members.length; ++i) { - var member = this.members[i]; - - checksumStrings.push(member.address + member.status + member.incarnationNumber); - } - - return checksumStrings.sort().join(';'); -}; - -Membership.prototype.getJoinPosition = function getJoinPosition() { - return Math.floor(Math.random() * (this.members.length - 0)) + 0; -}; - -Membership.prototype.getLocalMemberAddress = function getLocalMemberAddress() { - return this.localMember && this.localMember.address; -}; - -Membership.prototype.getMemberAt = function getMemberAt(index) { - return this.members[index]; -}; - -Membership.prototype.getMemberCount = function getMemberCount() { - return this.members.length; -}; - -Membership.prototype.getRandomPingableMembers = function(n, excluding) { - // TODO Revisit to make faster - return _.chain(this.members) - .reject(function(member) { return excluding.indexOf(member.address) > -1; }) - .filter(function(member) { return Membership.isPingable(member); }) - .sample(n) - .value(); -}; - -Membership.prototype.getState = function() { - return this.members.map(function(member) { - return { - address: member.address, - status: member.status, - incarnationNumber: member.incarnationNumber - }; - }); -}; - -Membership.prototype.getStats = function getStats() { - return { - checksum: this.checksum, - members: this.getState().sort(function (a, b) { - return a.address.localeCompare(b.address); - }), - version: this.version - }; -}; - -Membership.prototype.hasMember = function hasMember(member) { - return !!this.findMemberByAddress(member.address); -}; - -// Alive operation requires only an address. Incarnation number can be -// passed optionally, but nodes should never bump the incarnation number -// of a remote member. Incarnation number, for now, is only available -// for ease of testing. -Membership.prototype.makeAlive = function makeAlive(address, incarnationNumber) { - var member = this.findMemberByAddress(address); - - if (member) { - this.update([{ - address: member.address, - incarnationNumber: incarnationNumber || member.incarnationNumber, - status: 'alive' - }]); - } -}; - -Membership.prototype.makeFaulty = function makeFaulty(address) { - var member = this.findMemberByAddress(address); - - if (member) { - this.update([{ - address: member.address, - incarnationNumber: member.incarnationNumber, - status: 'faulty' - }]); - } -}; - -// Join operation requires both an address and initial incarnation number -Membership.prototype.makeJoin = function makeJoin(address, incarnationNumber) { - if (!address || !incarnationNumber) { - return; - } - - this.update([{ - address: address, - status: 'alive', - incarnationNumber: incarnationNumber - }]); -}; - -// Leave operations are only allowed on the local member -Membership.prototype.makeLeave = function makeLeave() { - this.update([{ - address: this.localMember.address, - status: 'leave', - incarnationNumber: +new Date() - }]); -}; - -// Suspect operation requires no change of the incarnation number -Membership.prototype.makeSuspect = function makeSuspect(address) { - var member = this.findMemberByAddress(address); - - if (member) { - this.update([{ - address: member.address, - incarnationNumber: member.incarnationNumber, - status: 'suspect' - }]); - } -}; - -Membership.prototype.shuffle = function shuffle() { - this.members = _.shuffle(this.members); -}; - -Membership.prototype.toString = function toString() { - return JSON.stringify(_.pluck(this.members, 'address')); -}; - -Membership.prototype.update = function update(changes) { - changes = Array.isArray(changes) ? changes : []; - this.ringpop.stat('gauge', 'changes.apply', changes.length); - - if (changes.length === 0) { - return; - } - - var updates = []; - - for (var i = 0 ; i < changes.length; i++) { - var change = changes[i]; - - var member = this.findMemberByAddress(change.address); - - if (!member) { - // This is the first time ringpop is hearing about - // this member. No need to evaluate override rules. - member = this.addMember({ - address: change.address, - status: change.status, - incarnationNumber: change.incarnationNumber - }, true, true); - - updates.push(member); - continue; - } - - var override = Membership.evalOverride(member, change); - - if (override) { - updates.push(override); - } - } - - if (updates.length > 0) { - this._emitUpdated(updates); - } -}; - -Membership.prototype._emitUpdated = function _emitUpdated(updates) { - updates = Array.isArray(updates) ? updates : [updates]; - - this.version++; - this.computeChecksum(); - - this.emit('updated', updates); -}; - -module.exports = { - Dissemination: Dissemination, - MemberIterator: MemberIterator, - Membership: Membership, -}; diff --git a/test/local-incarnation-test.js b/lib/membership-iterator.js similarity index 57% rename from test/local-incarnation-test.js rename to lib/membership-iterator.js index d953d1c8..55b56424 100644 --- a/test/local-incarnation-test.js +++ b/lib/membership-iterator.js @@ -17,31 +17,40 @@ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. + 'use strict'; -// Test dependencies -var Member = require('../lib/member.js'); +var LOG_10 = Math.log(10); -// Test helpers -var testRingpop = require('./lib/test-ringpop.js'); +function MembershipIterator(ring) { + this.ring = ring; + this.currentIndex = -1; + this.currentRound = 0; +} -function assertIncarnationNumber(deps, assert, memberStatus) { - var membership = deps.membership; - var local = membership.localMember; - var prevInc = local.incarnationNumber - 1; +MembershipIterator.prototype.next = function next() { + var membersVisited = {}; + var maxMembersToVisit = this.ring.membership.getMemberCount(); - membership.update([{ - address: local.address, - status: memberStatus - }]); + while (Object.keys(membersVisited).length < maxMembersToVisit) { + this.currentIndex++; - assert.ok(prevInc, 'prev incarnation number is truthy'); -} + if (this.currentIndex >= this.ring.membership.getMemberCount()) { + this.currentIndex = 0; + this.currentRound++; + this.ring.membership.shuffle(); + } + + var member = this.ring.membership.getMemberAt(this.currentIndex); + + membersVisited[member.address] = true; + + if (this.ring.membership.isPingable(member)) { + return member; + } + } -testRingpop('suspect update does not bump local incarnation number', function t(deps, assert) { - assertIncarnationNumber(deps, assert, Member.Status.Suspect); -}); + return null; +}; -testRingpop('faulty update does not bump local incarnation number', function t(deps, assert) { - assertIncarnationNumber(deps, assert, Member.Status.Faulty); -}); +module.exports = MembershipIterator; diff --git a/lib/membership_update_rollup.js b/lib/membership-update-rollup.js similarity index 100% rename from lib/membership_update_rollup.js rename to lib/membership-update-rollup.js diff --git a/lib/membership-update-rules.js b/lib/membership-update-rules.js new file mode 100644 index 00000000..17e0168b --- /dev/null +++ b/lib/membership-update-rules.js @@ -0,0 +1,68 @@ +// Copyright (c) 2015 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +'use strict'; + +var Member = require('./member.js'); + +function isAliveOverride(member, change) { + return change.status === 'alive' && + Member.Status[member.status] && + change.incarnationNumber > member.incarnationNumber; +}; + +function isFaultyOverride(member, change) { + return change.status === 'faulty' && + ((member.status === 'suspect' && change.incarnationNumber >= member.incarnationNumber) || + (member.status === 'faulty' && change.incarnationNumber > member.incarnationNumber) || + (member.status === 'alive' && change.incarnationNumber >= member.incarnationNumber)); +}; + +function isLeaveOverride(member, change) { + return change.status === 'leave' && + Member.Status[member.status] && + change.incarnationNumber >= member.incarnationNumber; +}; + +function isLocalFaultyOverride(ringpop, member, change) { + return ringpop.whoami() === member.address && + change.status === Member.Status.faulty; +}; + +function isLocalSuspectOverride(ringpop, member, change) { + return ringpop.whoami() === member.address && + change.status === Member.Status.suspect; +}; + +function isSuspectOverride(member, change) { + return change.status === 'suspect' && + ((member.status === 'suspect' && change.incarnationNumber > member.incarnationNumber) || + (member.status === 'faulty' && change.incarnationNumber > member.incarnationNumber) || + (member.status === 'alive' && change.incarnationNumber >= member.incarnationNumber)); +}; + +module.exports = { + isAliveOverride: isAliveOverride, + isFaultyOverride: isFaultyOverride, + isLeaveOverride: isLeaveOverride, + isLocalFaultyOverride: isLocalFaultyOverride, + isLocalSuspectOverride: isLocalSuspectOverride, + isSuspectOverride: isSuspectOverride +}; diff --git a/lib/membership.js b/lib/membership.js new file mode 100644 index 00000000..9baf9813 --- /dev/null +++ b/lib/membership.js @@ -0,0 +1,274 @@ +// Copyright (c) 2015 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +'use strict'; + +var _ = require('underscore'); +var EventEmitter = require('events').EventEmitter; +var farmhash = require('farmhash'); +var Member = require('./member.js'); +var MembershipUpdateRules = require('./membership-update-rules.js'); +var util = require('util'); + +function Membership(ringpop) { + this.ringpop = ringpop; + this.members = []; + this.checksum = null; +} + +util.inherits(Membership, EventEmitter); + +Membership.prototype.computeChecksum = function computeChecksum() { + /* The membership checksum is a farmhash of the checksum string computed + * for each member then joined with all other member checksum strings by ';'. + * As an example, the checksum string for a member might be: + * + * localhost:3000alive1414142122274 + * + * And joined together with other members: + * + * localhost:3000alive1414142122274;localhost:3001alive1414142122275 + * + * The member fields that are part of the checksum string are: address, status and + * incarnation number. + */ + var start = new Date(); + + this.checksum = farmhash.hash32(this.generateChecksumString()); + + this.ringpop.stat('timing', 'compute-checksum', start); + this.ringpop.stat('gauge', 'checksum', this.checksum); + + return this.checksum; +}; + +Membership.prototype.findMemberByAddress = function findMemberByAddress(address) { + // TODO Index by address + for (var i = 0; i < this.members.length; i++) { + var member = this.members[i]; + + if (member.address === address) { + return member; + } + } + + return null; +}; + +Membership.prototype.generateChecksumString = function generateChecksumString() { + var checksumStrings = []; + + for (var i = 0; i < this.members.length; ++i) { + var member = this.members[i]; + + checksumStrings.push(member.address + member.status + member.incarnationNumber); + } + + return checksumStrings.sort().join(';'); +}; + +Membership.prototype.getLocalMemberAddress = function getLocalMemberAddress() { + return this.localMember && this.localMember.address; +}; + +Membership.prototype.getJoinPosition = function getJoinPosition(ringpop) { + return Math.floor(Math.random() * (this.members.length - 0)) + 0; +}; + +Membership.prototype.getMemberAt = function getMemberAt(index) { + return this.members[index]; +}; + +Membership.prototype.getMemberCount = function getMemberCount() { + return this.members.length; +}; + +Membership.prototype.getRandomPingableMembers = function(n, excluding) { + var self = this; + + // TODO Revisit to make faster + return _.chain(this.members) + .reject(function(member) { return excluding.indexOf(member.address) > -1; }) + .filter(function(member) { return self.isPingable(member); }) + .sample(n) + .value(); +}; + +Membership.prototype.getStats = function getStats() { + return { + checksum: this.checksum, + members: this.members.sort(function (a, b) { + return a.address.localeCompare(b.address); + }) + }; +}; + +Membership.prototype.hasMember = function hasMember(member) { + return !!this.findMemberByAddress(member.address); +}; + +Membership.prototype.isPingable = function isPingable(member) { + return member.address !== this.ringpop.whoami() + && (member.status === 'alive' || + member.status === 'suspect'); +}; + +Membership.prototype.makeAlive = function makeAlive(address, incarnationNumber, source) { + return this.update({ + source: source || this.ringpop.whoami(), + address: address, + status: Member.Status.alive, + incarnationNumber: incarnationNumber, + timestamp: Date.now() + }); +}; + +Membership.prototype.makeFaulty = function makeFaulty(address, incarnationNumber, source) { + return this.update({ + source: source || this.ringpop.whoami(), + address: address, + status: Member.Status.faulty, + incarnationNumber: incarnationNumber, + timestamp: Date.now() + }); +}; + +Membership.prototype.makeLeave = function makeLeave(address, incarnationNumber, source) { + return this.update({ + source: source || this.ringpop.whoami(), + address: address, + status: Member.Status.leave, + incarnationNumber: incarnationNumber, + timestamp: Date.now() + }); +}; + +Membership.prototype.makeSuspect = function makeSuspect(address, incarnationNumber, source) { + return this.update({ + source: source || this.ringpop.whoami(), + address: address, + status: Member.Status.suspect, + incarnationNumber: incarnationNumber, + timestamp: Date.now() + }); +}; + +Membership.prototype.update = function update(changes) { + changes = Array.isArray(changes) ? changes : [changes]; + + this.ringpop.stat('gauge', 'changes.apply', changes.length); + + if (changes.length === 0) { + return; + } + + // Changes will be evaluated against membership update rules. + // Not all changes will be applied. + var self = this; + var updates = []; + + for (var i = 0 ; i < changes.length; i++) { + var change = changes[i]; + + var member = this.findMemberByAddress(change.address); + + // If first time seeing member, take change wholesale. + if (!member) { + makeUpdate(change); + updates.push(change); + continue; + } + + // If is local override, reassert that member is alive! + if (MembershipUpdateRules.isLocalSuspectOverride(this.ringpop, member, change) || + MembershipUpdateRules.isLocalFaultyOverride(this.ringpop, member, change)) { + var assertion = { + status: Member.Status.alive, + incarnationNumber: Date.now() + }; + + makeUpdate(_.extend(change, assertion)); + updates.push(change); + continue; + } + + // If non-local update, take change wholesale. + if (MembershipUpdateRules.isAliveOverride(member, change) || + MembershipUpdateRules.isSuspectOverride(member, change) || + MembershipUpdateRules.isFaultyOverride(member, change) || + MembershipUpdateRules.isLeaveOverride(member, change)) { + makeUpdate(change); + updates.push(change); + } + } + + if (updates.length > 0) { + this.computeChecksum(); + this.emit('updated', updates); + } + + return updates; + + function makeUpdate(update) { + var address = update.address; + var incarnationNumber = update.incarnationNumber; + + if (typeof address === 'undefined' || + address === null || + typeof incarnationNumber === 'undefined' || + incarnationNumber === null) { + // TODO Maybe throw? + return; + } + + var member = self.findMemberByAddress(address); + + if (!member) { + member = { + address: address, + status: update.status, + incarnationNumber: incarnationNumber + }; + + // TODO localMember is carried around as a convenience. + // Get rid of it eventually. + if (member.address === self.ringpop.whoami()) { + self.localMember = member; + } + + self.members.splice(self.getJoinPosition(self.ringpop), 0, member); + } + + member.status = update.status; + member.incarnationNumber = incarnationNumber; + + return member; + } +} + +Membership.prototype.shuffle = function shuffle() { + this.members = _.shuffle(this.members); +}; + +Membership.prototype.toString = function toString() { + return JSON.stringify(_.pluck(this.members, 'address')); +}; + +module.exports = Membership; diff --git a/lib/swim/ping-req-recvr.js b/lib/swim/ping-req-recvr.js index 336cc664..40b499a0 100644 --- a/lib/swim/ping-req-recvr.js +++ b/lib/swim/ping-req-recvr.js @@ -45,7 +45,7 @@ module.exports = function recvPingReq(opts, callback) { } callback(null, { - changes: ringpop.issueMembershipChanges(checksum, source), + changes: ringpop.dissemination.issueChanges(checksum, source), pingStatus: isOk, target: target }); diff --git a/lib/swim/ping-req-sender.js b/lib/swim/ping-req-sender.js index e582dc27..2e01613c 100644 --- a/lib/swim/ping-req-sender.js +++ b/lib/swim/ping-req-sender.js @@ -56,7 +56,7 @@ var PingReqPingError = TypedError({ function body(sender) { return JSON.stringify({ checksum: sender.ring.membership.checksum, - changes: sender.ring.issueMembershipChanges(), + changes: sender.ring.dissemination.issueChanges(), source: sender.ring.whoami(), target: sender.target.address }); @@ -229,7 +229,8 @@ module.exports = function sendPingReq(opts, callback) { unreachableMemberInfo: unreachableMemberInfo }); - ringpop.membership.makeSuspect(unreachableMember.address); + ringpop.membership.makeSuspect(unreachableMember.address, + unreachableMember.incarnationNumber); calledBack = true; callback(null, { diff --git a/lib/swim/ping-sender.js b/lib/swim/ping-sender.js index f1c3c7af..8cb896da 100644 --- a/lib/swim/ping-sender.js +++ b/lib/swim/ping-sender.js @@ -29,7 +29,7 @@ function PingSender(ring, member, callback) { host: this.address, timeout: ring.pingTimeout }; - var changes = ring.issueMembershipChanges(); + var changes = ring.dissemination.issueChanges(); var body = JSON.stringify({ checksum: ring.membership.checksum, changes: changes, diff --git a/lib/swim/suspicion.js b/lib/swim/suspicion.js index 7ec602f9..e3ac997c 100644 --- a/lib/swim/suspicion.js +++ b/lib/swim/suspicion.js @@ -64,7 +64,9 @@ Suspicion.prototype.start = function start(member) { var self = this; this.timers[member.address] = setTimeout(function() { - self.ringpop.membership.makeFaulty(member.address); + self.ringpop.membership.makeFaulty(member.address, + member.incarnationNumber); + self.ringpop.logger.info('ringpop member declares member faulty', { local: self.ringpop.whoami(), faulty: member.address diff --git a/test/index_test.js b/test/index_test.js index 7a534fc4..60369b6d 100644 --- a/test/index_test.js +++ b/test/index_test.js @@ -52,7 +52,7 @@ test('does not throw when calling lookup with an integer', function t(assert) { test('key hashes to only server', function t(assert) { var ringpop = createRingpop(); - ringpop.addLocalMember(); + ringpop.membership.makeAlive(ringpop.whoami(), Date.now()); assert.equals(ringpop.lookup(12345), ringpop.hostPort, 'hashes to only server'); ringpop.destroy(); assert.end(); @@ -62,7 +62,7 @@ test('admin join rejoins if member has previously left', function t(assert) { assert.plan(3); var ringpop = createRingpop(); - ringpop.addLocalMember({ incarnationNumber: 1 }); + ringpop.membership.makeAlive(ringpop.whoami(), 1); ringpop.adminLeave(function(err, res1, res2) { assert.equals(res2, 'ok', 'node left cluster'); @@ -93,8 +93,8 @@ test('admin leave prevents redundant leave', function t(assert) { assert.plan(2); var ringpop = createRingpop(); - ringpop.addLocalMember({ incarnationNumber: 1 }); - ringpop.membership.makeLeave(); + ringpop.membership.makeAlive(ringpop.whoami(), 1); + ringpop.membership.makeLeave(ringpop.whoami(), 1); ringpop.adminLeave(function(err) { assert.ok(err, 'an error occurred'); assert.equals(err.type, 'ringpop.invalid-leave.redundant', 'cannot leave cluster twice'); @@ -107,7 +107,7 @@ test('admin leave makes local member leave', function t(assert) { assert.plan(3); var ringpop = createRingpop(); - ringpop.addLocalMember({ incarnationNumber: 1 }); + ringpop.membership.makeAlive(ringpop.whoami(), 1); ringpop.adminLeave(function(err, _, res2) { assert.notok(err, 'an error did not occur'); assert.ok('leave', ringpop.membership.localMember.status, 'local member has correct status'); @@ -121,7 +121,7 @@ test('admin leave stops gossip', function t(assert) { assert.plan(2); var ringpop = createRingpop(); - ringpop.addLocalMember({ incarnationNumber: 1 }); + ringpop.membership.makeAlive(ringpop.whoami(), 1); ringpop.gossip.start(); ringpop.adminLeave(function(err) { assert.notok(err, 'an error did not occur'); @@ -135,14 +135,11 @@ test('admin leave stops suspicion subprotocol', function t(assert) { assert.plan(2); var ringpopRemote = createRemoteRingpop(); - ringpopRemote.addLocalMember(); + ringpopRemote.membership.makeAlive(ringpopRemote.whoami(), Date.now()); var ringpop = createRingpop(); - ringpop.addLocalMember({ incarnationNumber: 1 }); - ringpop.membership.addMember({ - address: ringpopRemote.membership.localMember.address, - status: 'alive' - }); + ringpop.membership.makeAlive(ringpop.whoami(), 1); + ringpop.membership.makeAlive(ringpopRemote.whoami(), Date.now()); ringpop.suspicion.start(ringpopRemote.hostPort); ringpop.adminLeave(function(err) { @@ -312,11 +309,16 @@ test('emits membership changed event', function t(assert) { var node1Addr = '127.0.0.1:3001'; var ringpop = createRingpop(); - ringpop.addLocalMember(); - ringpop.membership.addMember({ - address: node1Addr, - status: 'alive' - }); + ringpop.membership.makeAlive(ringpop.whoami(), Date.now()); + ringpop.membership.makeAlive(node1Addr, Date.now()); + + assertChanged(); + + var node1Member = ringpop.membership.findMemberByAddress(node1Addr); + ringpop.membership.makeSuspect(node1Addr, node1Member.incarnationNumber); + + ringpop.destroy(); + assert.end(); function assertChanged() { ringpop.once('membershipChanged', function onMembershipChanged() { @@ -327,12 +329,6 @@ test('emits membership changed event', function t(assert) { assert.fail('no ring changed'); }); } - - assertChanged(); - ringpop.membership.makeSuspect(node1Addr); - - ringpop.destroy(); - assert.end(); }); test('emits ring changed event', function t(assert) { @@ -340,13 +336,11 @@ test('emits ring changed event', function t(assert) { var node1Addr = '127.0.0.1:3001'; var node2Addr = '127.0.0.1:3002'; + var magicIncNo = Date.now() + 123456; var ringpop = createRingpop(); - ringpop.addLocalMember(); - ringpop.membership.addMember({ - address: node1Addr, - status: 'alive' - }); + ringpop.membership.makeAlive(ringpop.whoami(), Date.now()); + ringpop.membership.makeAlive(node1Addr, Date.now()); function assertChanged(changer) { ringpop.once('membershipChanged', function onMembershipChanged() { @@ -365,15 +359,15 @@ test('emits ring changed event', function t(assert) { }); assertChanged(function assertIt() { - ringpop.membership.makeAlive(node1Addr, Date.now() + 123456 /* magic incarnation number bump */); + ringpop.membership.makeAlive(node1Addr, magicIncNo); }); assertChanged(function assertIt() { - ringpop.membership.makeLeave(node1Addr); + ringpop.membership.makeLeave(node1Addr, magicIncNo); }); assertChanged(function assertIt() { - ringpop.membership.makeJoin(node2Addr, Date.now()); + ringpop.membership.makeAlive(node2Addr, Date.now()); }); ringpop.destroy(); @@ -382,14 +376,10 @@ test('emits ring changed event', function t(assert) { test('first time member, not alive', function t(assert) { var ringpop = createRingpop(); - ringpop.addLocalMember(); + ringpop.membership.makeAlive(ringpop.whoami(), Date.now()); var faultyAddr = '127.0.0.1:3001'; - ringpop.membership.update([{ - address: faultyAddr, - status: 'faulty', - incarnationNumber: Date.now() - }]); + ringpop.membership.makeFaulty(faultyAddr, Date.now()); assert.notok(ringpop.ring.hasServer(faultyAddr), 'new faulty server should not be in ring'); diff --git a/test/integration/join-test.js b/test/integration/join-test.js index 872377a8..e89048a6 100644 --- a/test/integration/join-test.js +++ b/test/integration/join-test.js @@ -128,7 +128,7 @@ testRingpopCluster({ callback(null, { app: 'test', coordinator: cluster[1].hostPort, - membership: cluster[1].membership.getState() + membership: cluster[1].dissemination.fullSync() }); }, 100); }; diff --git a/test/lib/test-ringpop.js b/test/lib/test-ringpop.js index b09960fd..5626ee7e 100644 --- a/test/lib/test-ringpop.js +++ b/test/lib/test-ringpop.js @@ -35,9 +35,11 @@ function testRingpop(opts, name, test) { hostPort: opts.hostPort || '127.0.0.1:3000' }); - ringpop.addLocalMember(); + ringpop.membership.makeAlive(ringpop.whoami(), Date.now()); test({ + dissemination: ringpop.dissemination, + iterator: ringpop.memberIterator, localMember: ringpop.membership.localMember, membership: ringpop.membership, ringpop: ringpop diff --git a/test/member_iterator_test.js b/test/member_iterator_test.js deleted file mode 100644 index b49ef4a4..00000000 --- a/test/member_iterator_test.js +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright (c) 2015 Uber Technologies, Inc. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. -var MemberIterator = require('../lib/members').MemberIterator; -var test = require('tape'); - -function createMembers2() { - return [ - { address: '127.0.0.1', status: 'alive', isLocal: false }, - { address: '127.0.0.2', status: 'alive', isLocal: true } - ]; -} - -function createMembers3() { - return [ - { address: '127.0.0.1', status: 'alive', isLocal: false }, - { address: '127.0.0.2', status: 'alive', isLocal: false }, - { address: '127.0.0.3', status: 'alive', isLocal: true } - ]; -} - -function createRing(members) { - return { - membership: { - getMemberCount: function() { - return members.length; - }, - getMemberAt: function(index) { - return members[index]; - }, - shuffle: function() { - return members.reverse(); - } - } - }; -} - -test('iterates over two members correctly', function t(assert) { - var ring = createRing(createMembers2()); - var iterator = new MemberIterator(ring); - - assert.equals(iterator.next().address, '127.0.0.1', 'first member is first'); - assert.equals(iterator.next().address, '127.0.0.1', 'first member is next'); - assert.equals(iterator.next().address, '127.0.0.1', 'first member is next again'); - assert.equals(iterator.next().address, '127.0.0.1', 'first member is last'); - assert.end(); -}); - -test('iterates over three members correctly', function t(assert) { - var ring = createRing(createMembers3()); - var iterator = new MemberIterator(ring); - - assert.equals(iterator.next().address, '127.0.0.1', 'first member is first'); - assert.equals(iterator.next().address, '127.0.0.2', 'second member is next'); - assert.equals(iterator.next().address, '127.0.0.2', 'second member is next again'); - assert.equals(iterator.next().address, '127.0.0.1', 'first member is next'); - assert.equals(iterator.next().address, '127.0.0.1', 'first member is next again'); - assert.equals(iterator.next().address, '127.0.0.2', 'second member is last'); - assert.end(); -}); - -test('skips over 2 faulty members and 1 local member', function t(assert) { - var members = createMembers3(); - members[0].status = 'faulty'; - members[1].status = 'faulty'; - var ring = createRing(members); - var iterator = new MemberIterator(ring); - - assert.equals(iterator.next(), null, 'next member is null'); - assert.equals(iterator.next(), null, 'next member is null again'); - assert.end(); -}); diff --git a/test/members_test.js b/test/members_test.js deleted file mode 100644 index c97971fc..00000000 --- a/test/members_test.js +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright (c) 2015 Uber Technologies, Inc. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. -var Membership = require('../lib/members').Membership; -var test = require('tape'); - -var ringpop = { - stat: function() {} -}; - -test('checksum is changed when membership is updated', function t(assert) { - var membership = new Membership(ringpop); - membership.update([{ address: '127.0.0.1:3000', status: 'alive' }]) - var prevChecksum = membership.checksum; - membership.update([{ address: '127.0.0.1:3001', status: 'alive' }]) - - assert.doesNotEqual(membership.checksum, prevChecksum, 'checksum is changed'); - assert.end(); -}); - -test('change with higher incarnation number results in leave override', function t(assert) { - var member = { status: 'alive', incarnationNumber: 1 }; - var change = { status: 'leave', incarnationNumber: 2 }; - - var update = Membership.evalOverride(member, change); - - assert.equals(update.status, 'leave', 'results in leave'); - assert.end(); -}); - -test('change with same incarnation number does not result in leave override', function t(assert) { - var member = { status: 'alive', incarnationNumber: 1 }; - var change = { status: 'leave', incarnationNumber: 1 }; - - var update = Membership.evalOverride(member, change); - - assert.notok(update, 'no override'); - assert.end(); -}); - -test('change with lower incarnation number does not result in leave override', function t(assert) { - var member = { status: 'alive', incarnationNumber: 1 }; - var change = { status: 'leave', incarnationNumber: 0 }; - - var update = Membership.evalOverride(member, change); - - assert.notok(update, 'no override'); - assert.end(); -}); - -test('member is able to go from alive to faulty without going through suspect', function t(assert) { - var aliveMember = { - status: 'alive', - incarnationNumber: 0 - }; - - assert.notok(Membership.evalOverride(aliveMember, { - status: 'faulty', - incarnationNumber: -1 - }), 'no override when incarnation number is lower'); - - assert.ok(Membership.evalOverride(aliveMember, { - status: 'faulty', - incarnationNumber: 0 - }), 'override when incarnation number is same'); - - assert.end(); -}); diff --git a/test/membership-iterator-test.js b/test/membership-iterator-test.js new file mode 100644 index 00000000..7505c36a --- /dev/null +++ b/test/membership-iterator-test.js @@ -0,0 +1,71 @@ +// Copyright (c) 2015 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +var MembershipIterator = require('../lib/membership-iterator.js'); +var testRingpop = require('./lib/test-ringpop.js'); + +testRingpop('iterates over two members correctly', function t(deps, assert) { + var ringpop = deps.ringpop; + var membership = deps.membership; + var iterator = deps.iterator; + + membership.makeAlive('127.0.0.1:3001', Date.now()); + membership.makeAlive('127.0.0.1:3002', Date.now()); + + var iterated = {}; + iterated[iterator.next().address] = true; + iterated[iterator.next().address] = true; + + assert.equals(Object.keys(iterated).length, 2, '2 members iterated over'); +}); + +testRingpop('iterates over three members correctly', function t(deps, assert) { + var ringpop = deps.ringpop; + var membership = deps.membership; + var iterator = deps.iterator; + + membership.makeAlive('127.0.0.1:3001', Date.now()); + membership.makeAlive('127.0.0.1:3002', Date.now()); + membership.makeAlive('127.0.0.1:3003', Date.now()); + + var iterated = {}; + iterated[iterator.next().address] = true; + iterated[iterator.next().address] = true; + iterated[iterator.next().address] = true; + + assert.equals(Object.keys(iterated).length, 3, '3 members iterated over'); +}); + +testRingpop('skips over faulty member and 1 local member', function t(deps, assert) { + var ringpop = deps.ringpop; + var membership = deps.membership; + var iterator = deps.iterator; + + membership.makeAlive('127.0.0.1:3001', Date.now()); + membership.makeFaulty('127.0.0.1:3002', Date.now()); + membership.makeAlive('127.0.0.1:3003', Date.now()); + + var iterated = {}; + iterated[iterator.next().address] = true; + iterated[iterator.next().address] = true; + iterated[iterator.next().address] = true; + + assert.equals(Object.keys(iterated).length, 2, '2 members iterated over'); + assert.notok(iterated['127.0.0.1:3002'], 'faulty member not iterated over'); +}); diff --git a/test/membership-test.js b/test/membership-test.js new file mode 100644 index 00000000..424baf20 --- /dev/null +++ b/test/membership-test.js @@ -0,0 +1,140 @@ +// Copyright (c) 2015 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +'use strict'; + +// Test dependencies +var Member = require('../lib/member.js'); +var MembershipUpdateRules = require('../lib/membership-update-rules.js'); + +// Test helpers +var testRingpop = require('./lib/test-ringpop.js'); + +function assertIncarnationNumber(deps, assert, memberStatus) { + var membership = deps.membership; + var local = membership.localMember; + var prevInc = local.incarnationNumber - 1; + + membership.update({ + address: local.address, + status: memberStatus, + incarnationNumber: local.incarnatioNumber + }); + + assert.ok(prevInc, 'prev incarnation number is truthy'); +} + +testRingpop('suspect update does not bump local incarnation number', function t(deps, assert) { + assertIncarnationNumber(deps, assert, Member.Status.suspect); +}); + +testRingpop('faulty update does not bump local incarnation number', function t(deps, assert) { + assertIncarnationNumber(deps, assert, Member.Status.faulty); +}); + +testRingpop('checksum is changed when membership is updated', function t(deps, assert) { + var membership = deps.membership; + + membership.makeAlive('127.0.0.1:3000', Date.now()); + var prevChecksum = membership.checksum; + + membership.makeAlive('127.0.0.1:3001', Date.now()); + + assert.doesNotEqual(membership.checksum, prevChecksum, 'checksum is changed'); +}); + +testRingpop('change with higher incarnation number results in leave override', function t(deps, assert) { + var ringpop = deps.ringpop; + var dissemination = deps.dissemination; + var membership = deps.membership; + + var member = membership.findMemberByAddress(ringpop.whoami()); + assert.equals(member.status, Member.Status.alive, 'member starts alive'); + + membership.update([{ + address: ringpop.whoami(), + status: Member.Status.leave, + incarnationNumber: member.incarnationNumber + 1 + }]); + + assert.equals(member.status, Member.Status.leave, 'results in leave'); +}); + +testRingpop('change with same incarnation number does not result in leave override', function t(deps, assert) { + var ringpop = deps.ringpop; + var dissemination = deps.dissemination; + var membership = deps.membership; + + var member = membership.findMemberByAddress(ringpop.whoami()); + assert.equals(member.status, Member.Status.alive, 'member starts alive'); + + membership.update([{ + address: ringpop.whoami(), + status: Member.Status.Leave, + incarnationNumber: member.incarnationNumber + }]); + + assert.equals(member.status, Member.Status.alive, 'results in no leave'); +}); + +testRingpop('change with lower incarnation number does not result in leave override', function t(deps, assert) { + var ringpop = deps.ringpop; + var dissemination = deps.dissemination; + var membership = deps.membership; + + var member = membership.findMemberByAddress(ringpop.whoami()); + assert.equals(member.status, Member.Status.alive, 'member starts alive'); + + membership.update([{ + address: ringpop.whoami(), + status: Member.Status.Leave, + incarnationNumber: member.incarnationNumber - 1 + }]); + + assert.equals(member.status, Member.Status.alive, 'results in no leave'); +}); + +testRingpop('member is able to go from alive to faulty without going through suspect', function t(deps, assert) { + var ringpop = deps.ringpop; + var dissemination = deps.dissemination; + var membership = deps.membership; + + var newMemberAddr = '127.0.0.1:3001'; + membership.makeAlive(newMemberAddr, Date.now()); + + var newMember = membership.findMemberByAddress(newMemberAddr); + assert.equals(newMember.status, Member.Status.alive, 'member starts alive'); + + membership.update([{ + address: newMember.address, + status: Member.Status.faulty, + incarnationNumber: newMember.incarnationNumber - 1 + }]); + + assert.equals(newMember.status, Member.Status.alive, 'no override with lower inc no.'); + + membership.update([{ + address: newMember.address, + status: Member.Status.faulty, + incarnationNumber: newMember.incarnationNumber + }]); + + assert.equals(newMember.status, Member.Status.faulty, 'override with same inc no.'); +}); diff --git a/test/membership_update_rollup_test.js b/test/membership-update-rollup-test.js similarity index 98% rename from test/membership_update_rollup_test.js rename to test/membership-update-rollup-test.js index efc849e8..70cf0f22 100644 --- a/test/membership_update_rollup_test.js +++ b/test/membership-update-rollup-test.js @@ -20,7 +20,7 @@ 'use strict'; var after = require('after'); -var MembershipUpdateRollup = require('../lib/membership_update_rollup.js'); +var MembershipUpdateRollup = require('../lib/membership-update-rollup.js'); var Ringpop = require('../index.js'); var test = require('tape');