Skip to content

Commit

Permalink
Beef up the Member class
Browse files Browse the repository at this point in the history
  • Loading branch information
jwolski committed Aug 26, 2015
1 parent db6491a commit 6d44507
Show file tree
Hide file tree
Showing 4 changed files with 179 additions and 152 deletions.
111 changes: 39 additions & 72 deletions lib/membership/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ var _ = require('underscore');
var EventEmitter = require('events').EventEmitter;
var farmhash = require('farmhash');
var Member = require('./member.js');
var MembershipUpdateRules = require('./rules.js');
var mergeMembershipChangesets = require('./merge.js');
var util = require('util');
var uuid = require('node-uuid');
Expand Down Expand Up @@ -120,12 +119,20 @@ Membership.prototype.getRandomPingableMembers = function(n, excluding) {
};

Membership.prototype.getStats = function getStats() {
var self = this;

return {
checksum: this.checksum,
members: this.members.sort(function (a, b) {
members: getMemberStats().sort(function (a, b) {
return a.address.localeCompare(b.address);
})
};

function getMemberStats() {
return self.members.map(function map(member) {
return member.getStats();
});
}
};

Membership.prototype.hasMember = function hasMember(member) {
Expand All @@ -139,20 +146,20 @@ Membership.prototype.isPingable = function isPingable(member) {
};

Membership.prototype.makeAlive = function makeAlive(address, incarnationNumber) {
return makeUpdate(this, address, incarnationNumber, Member.Status.alive,
return this._makeUpdate(address, incarnationNumber, Member.Status.alive,
address === this.ringpop.whoami());
};

Membership.prototype.makeFaulty = function makeFaulty(address, incarnationNumber) {
return makeUpdate(this, address, incarnationNumber, Member.Status.faulty);
return this._makeUpdate(address, incarnationNumber, Member.Status.faulty);
};

Membership.prototype.makeLeave = function makeLeave(address, incarnationNumber) {
return makeUpdate(this, address, incarnationNumber, Member.Status.leave);
return this._makeUpdate(address, incarnationNumber, Member.Status.leave);
};

Membership.prototype.makeSuspect = function makeSuspect(address, incarnationNumber) {
return makeUpdate(this, address, incarnationNumber, Member.Status.suspect);
return this._makeUpdate(address, incarnationNumber, Member.Status.suspect);
};

// Sets stashed updates. set() is different from update() in that it bypasses
Expand Down Expand Up @@ -185,10 +192,7 @@ Membership.prototype.set = function set() {
for (var i = 0; i < updates.length; i++) {
var update = updates[i];

var member = new Member(
update.address,
update.status,
update.incarnationNumber);
var member = new Member(this.ringpop, update);

this.members.push(member);
this.membersByAddress[member.address] = member;
Expand Down Expand Up @@ -233,34 +237,28 @@ Membership.prototype.update = function update(changes, isLocal) {

var member = this.findMemberByAddress(change.address);

// If first time seeing member, take change wholesale.
if (!member) {
applyUpdate(change);
updates.push(change);
continue;
}
member = new Member(this.ringpop, change);

// 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()
};
// localMember is carried around as a convenience.
if (member.address === this.ringpop.whoami()) {
this.localMember = member;
}

this.members.splice(this.getJoinPosition(), 0, member);
this.membersByAddress[member.address] = member;

// Note that I am invoking the 'updated' event handler here. There
// are two reasons for that. Firstly, what the handler does is
// necessary here too. Secondly, it is convenient to reuse it.
onMemberUpdated(change);

applyUpdate(_.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)) {
applyUpdate(change);
updates.push(change);
}
// One-time subscription for batching applied updates
member.once('updated', onMemberUpdated);
member.evaluateUpdate(change);
}

if (updates.length > 0) {
Expand All @@ -270,36 +268,7 @@ Membership.prototype.update = function update(changes, isLocal) {

return updates;

function applyUpdate(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 = new Member(address, update.status, 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(), 0, member);
self.membersByAddress[member.address] = member;
}

member.status = update.status;
member.incarnationNumber = incarnationNumber;

function onMemberUpdated(update) {
if (update.source !== self.ringpop.whoami()) {
self.ringpop.logger.debug('ringpop applied remote update', {
local: self.ringpop.whoami(),
Expand All @@ -308,7 +277,7 @@ Membership.prototype.update = function update(changes, isLocal) {
});
}

return member;
updates.push(update);
}
};

Expand All @@ -320,17 +289,15 @@ Membership.prototype.toString = function toString() {
return JSON.stringify(_.pluck(this.members, 'address'));
};

/* jshint maxparams: 5 */
function makeUpdate(membership, address, incarnationNumber, status, isLocal) {
var ringpop = membership.ringpop;

var localMember = membership.localMember || {
Membership.prototype._makeUpdate = function _makeUpate(address,
incarnationNumber, status, isLocal) {
var localMember = this.localMember || {
address: address,
incarnationNumber: incarnationNumber
};

var updateId = uuid.v4();
var updates = membership.update({
var updates = this.update({
id: updateId,
source: localMember.address,
sourceIncarnationNumber: localMember.incarnationNumber,
Expand All @@ -342,13 +309,13 @@ function makeUpdate(membership, address, incarnationNumber, status, isLocal) {

if (updates.length > 0) {
var logData = {};
logData.local = ringpop.whoami();
logData.local = this.ringpop.whoami();
logData[status] = address;
logData.updateId = updateId;
ringpop.logger.debug('ringpop member declares other member ' + status, logData);
this.ringpop.logger.debug('ringpop member declares other member ' + status, logData);
}

return updates;
}
};

module.exports = Membership;
104 changes: 100 additions & 4 deletions lib/membership/member.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,108 @@
// THE SOFTWARE.
'use strict';

function Member(address, status, incarnationNumber) {
this.address = address;
this.status = status;
this.incarnationNumber = incarnationNumber;
var _ = require('underscore');
var EventEmitter = require('events').EventEmitter;
var util = require('util');

function Member(ringpop, update) {
this.ringpop = ringpop;
this.address = update.address;
this.status = update.status;
this.incarnationNumber = update.incarnationNumber;
}

util.inherits(Member, EventEmitter);

// This function is named with the word "evaluate" because it is not
// guaranteed that the update will be applied. Naming it "update()"
// would have been misleading.
Member.prototype.evaluateUpdate = function evaluateUpdate(update) {
// The local override and "other" override rules that are evaluated
// here stem from the rules defined in the SWIM paper. They deviate
// a bit from that literature since Ringpop has added the "leave"
// status and retains faulty members in its membership list.
if (this._isLocalOverride(update)) {
// Override intended update. Assert aliveness!
update = _.defaults({
status: Member.Status.alive,
incarnationNumber: Date.now()
}, update);
} else if (!this._isOtherOverride(update)) {
return;
}

// We've got an update. Apply all-the-things.
if (this.status !== update.status) {
this.status = update.status;
}

if (this.incarnationNumber !== update.incarnationNumber) {
this.incarnationNumber = update.incarnationNumber;
}

this.emit('updated', update);

return true;
};

Member.prototype.getStats = function getStats() {
return {
address: this.address,
status: this.status,
incarnationNumber: this.incarnationNumber
};
};

Member.prototype._isLocalOverride = function _isLocalOverride(update) {
var self = this;

return isLocalFaultyOverride() || isLocalSuspectOverride();

function isLocalFaultyOverride() {
return self.ringpop.whoami() === self.address &&
update.status === Member.Status.faulty;
}

function isLocalSuspectOverride() {
return self.ringpop.whoami() === self.address &&
update.status === Member.Status.suspect;
}
};

Member.prototype._isOtherOverride = function _isOtherOverride(update) {
var self = this;

return isAliveOverride() || isSuspectOverride() || isFaultyOverride() ||
isLeaveOverride();

function isAliveOverride() {
return update.status === 'alive' &&
Member.Status[self.status] &&
update.incarnationNumber > self.incarnationNumber;
}

function isFaultyOverride() {
return update.status === 'faulty' &&
((self.status === 'suspect' && update.incarnationNumber >= self.incarnationNumber) ||
(self.status === 'faulty' && update.incarnationNumber > self.incarnationNumber) ||
(self.status === 'alive' && update.incarnationNumber >= self.incarnationNumber));
}

function isLeaveOverride() {
return update.status === 'leave' &&
self.status !== Member.Status.leave &&
update.incarnationNumber >= self.incarnationNumber;
}

function isSuspectOverride() {
return update.status === 'suspect' &&
((self.status === 'suspect' && update.incarnationNumber > self.incarnationNumber) ||
(self.status === 'faulty' && update.incarnationNumber > self.incarnationNumber) ||
(self.status === 'alive' && update.incarnationNumber >= self.incarnationNumber));
}
};

Member.Status = {
alive: 'alive',
faulty: 'faulty',
Expand Down
68 changes: 0 additions & 68 deletions lib/membership/rules.js

This file was deleted.

Loading

0 comments on commit 6d44507

Please sign in to comment.