Compare commits
1 Commits
Author | SHA1 | Date |
---|---|---|
Vitaliy Filippov | a0b20be22c |
65
tinyraft.js
65
tinyraft.js
|
@ -9,7 +9,8 @@
|
|||
// The only requirement is to guarantee preservation of entries confirmed by
|
||||
// all hosts participating in consensus.
|
||||
//
|
||||
// Supports leader expiration like in NuRaft:
|
||||
// Supports pre-vote protocol and leader expiration like in NuRaft:
|
||||
// https://github.com/eBay/NuRaft/blob/master/docs/prevote_protocol.md
|
||||
// https://github.com/eBay/NuRaft/blob/master/docs/leadership_expiration.md
|
||||
|
||||
const EventEmitter = require('events');
|
||||
|
@ -18,6 +19,10 @@ const VOTE_REQUEST = 'vote_request';
|
|||
const VOTE = 'vote';
|
||||
const PING = 'ping';
|
||||
const PONG = 'pong';
|
||||
// Following 3 are required only for the "pre-vote" feature
|
||||
const PRE_VOTE_REQUEST = 'pre_vote_request';
|
||||
const PRE_VOTE = 'pre_vote';
|
||||
const FOLLOW = 'follow';
|
||||
|
||||
const CANDIDATE = 'candidate';
|
||||
const LEADER = 'leader';
|
||||
|
@ -31,6 +36,7 @@ class TinyRaft extends EventEmitter
|
|||
// electionTimeout?: number,
|
||||
// heartbeatTimeout?: number,
|
||||
// leadershipTimeout?: number,
|
||||
// enablePrevote?: bool,
|
||||
// initialTerm?: number,
|
||||
// }
|
||||
constructor(config)
|
||||
|
@ -43,6 +49,7 @@ class TinyRaft extends EventEmitter
|
|||
this.randomTimeout = config.randomTimeout > 0 ? Number(config.randomTimeout) : this.electionTimeout;
|
||||
this.heartbeatTimeout = Number(config.heartbeatTimeout) || 1000;
|
||||
this.leadershipTimeout = Number(config.leadershipTimeout) || 0;
|
||||
this.enablePrevote = config.enablePrevote ? true : false;
|
||||
if (!this.nodeId || this.nodeId instanceof Object ||
|
||||
!(this.nodes instanceof Array) || this.nodes.filter(n => !n || n instanceof Object).length > 0 ||
|
||||
!(this.send instanceof Function))
|
||||
|
@ -52,6 +59,7 @@ class TinyRaft extends EventEmitter
|
|||
this.term = 0;
|
||||
this.state = null;
|
||||
this.leader = null;
|
||||
this.preVoting = false;
|
||||
}
|
||||
|
||||
_nextTerm(after)
|
||||
|
@ -67,10 +75,32 @@ class TinyRaft extends EventEmitter
|
|||
}
|
||||
}
|
||||
|
||||
_prevote()
|
||||
{
|
||||
this.leaderTimedOut = true;
|
||||
this.preVoting = true;
|
||||
this.preVoteOk = this.preVoteFail = 0;
|
||||
this.votes = { [this.nodeId]: [ this.nodeId ] };
|
||||
for (const node of this.nodes)
|
||||
{
|
||||
if (node != this.nodeId)
|
||||
{
|
||||
this.send(node, { type: PRE_VOTE_REQUEST, term: this.term });
|
||||
}
|
||||
}
|
||||
this._nextTerm(this.electionTimeout);
|
||||
}
|
||||
|
||||
start()
|
||||
{
|
||||
this._nextTerm(-1);
|
||||
this.electionTimer = null;
|
||||
if (this.enablePrevote && !this.preVoting)
|
||||
{
|
||||
this._prevote();
|
||||
return;
|
||||
}
|
||||
this.preVoting = false;
|
||||
this.term++;
|
||||
this.voted = 1;
|
||||
this.votes = { [this.nodeId]: [ this.nodeId ] };
|
||||
|
@ -123,7 +153,36 @@ class TinyRaft extends EventEmitter
|
|||
|
||||
onReceive(from, msg)
|
||||
{
|
||||
if (msg.type == VOTE_REQUEST)
|
||||
if (msg.type == PRE_VOTE_REQUEST)
|
||||
{
|
||||
this.send(from, { type: PRE_VOTE, term: this.term, leader: this.leaderTimedOut ? null : this.leader });
|
||||
}
|
||||
else if (msg.type == PRE_VOTE && this.preVoting)
|
||||
{
|
||||
if (msg.term == this.term && msg.leader)
|
||||
{
|
||||
this.preVoteOk++;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.preVoteFail++;
|
||||
}
|
||||
if (this.preVoteOk > this.nodes.length/2)
|
||||
{
|
||||
this.preVoting = false;
|
||||
this.preVoteOk = 0;
|
||||
this.preVoteFail = 0;
|
||||
this._nextTerm(this.electionTimeout);
|
||||
}
|
||||
if (this.preVoteFail > this.nodes.length/2)
|
||||
{
|
||||
this._nextTerm(-1);
|
||||
this.preVoteOk = 0;
|
||||
this.preVoteFail = 0;
|
||||
this.start();
|
||||
}
|
||||
}
|
||||
else if (msg.type == VOTE_REQUEST)
|
||||
{
|
||||
if (msg.term > this.term && msg.leader)
|
||||
{
|
||||
|
@ -240,6 +299,8 @@ class TinyRaft extends EventEmitter
|
|||
}
|
||||
}
|
||||
|
||||
TinyRaft.PRE_VOTE_REQUEST = PRE_VOTE_REQUEST;
|
||||
TinyRaft.PRE_VOTE = PRE_VOTE;
|
||||
TinyRaft.VOTE_REQUEST = VOTE_REQUEST;
|
||||
TinyRaft.VOTE = VOTE;
|
||||
TinyRaft.PING = PING;
|
||||
|
|
|
@ -10,7 +10,7 @@ function newNode(id, nodes, partitions)
|
|||
electionTimeout: 500,
|
||||
send: function(to, msg)
|
||||
{
|
||||
if (!partitions[n.nodeId+'-'+to] && !partitions[to+'-'+n.nodeId] && nodes[to])
|
||||
if (!partitions[n.nodeId+'-'+to] && nodes[to])
|
||||
{
|
||||
console.log('received from '+n.nodeId+' to '+to+': '+JSON.stringify(msg));
|
||||
setImmediate(function() { nodes[to].onReceive(n.nodeId, msg); });
|
||||
|
@ -148,10 +148,62 @@ async function testAddNode()
|
|||
console.log('testAddNode: OK');
|
||||
}
|
||||
|
||||
async function testPreVote()
|
||||
{
|
||||
// The original example for pre-vote protocol from here:
|
||||
// https://github.com/eBay/NuRaft/blob/master/docs/prevote_protocol.md
|
||||
// only has partitions between nodes 1-5, 2-4, 3-4.
|
||||
// But that setup has a problem of leader jumping between nodes 3 and 4
|
||||
// which can't be prevented by prevotes because 4 only has connections
|
||||
// to 2 nodes so they can't form a majority to reject the vote.
|
||||
const partitions = {
|
||||
'1-5': true,
|
||||
'2-4': true,
|
||||
'3-4': true,
|
||||
'2-3': true,
|
||||
'5-1': true,
|
||||
'4-2': true,
|
||||
'4-3': true,
|
||||
'3-2': true,
|
||||
};
|
||||
const nodes = newNodes(5, partitions);
|
||||
let leader = await new Promise((ok, no) =>
|
||||
{
|
||||
setTimeout(() => no(new Error('no leader in 500 ms')), 500);
|
||||
const chg = (st) =>
|
||||
{
|
||||
if (st.leader)
|
||||
{
|
||||
nodes[1].off('change', chg);
|
||||
if (st.leader != 1 && st.leader != 5)
|
||||
no('leader is not 1 or 5');
|
||||
ok(st.leader);
|
||||
}
|
||||
};
|
||||
nodes[1].on('change', chg);
|
||||
});
|
||||
console.log('OK: Got leader in 500 ms');
|
||||
await new Promise((ok, no) =>
|
||||
{
|
||||
setTimeout(ok, 5000);
|
||||
const chg = (st) =>
|
||||
{
|
||||
if (st.state != TinyRaft.FOLLOWER || st.leader != leader)
|
||||
{
|
||||
nodes[5].off('change', chg);
|
||||
no(new Error('leader changed in 5000 ms'));
|
||||
}
|
||||
};
|
||||
nodes[5].on('change', chg);
|
||||
});
|
||||
console.log('OK: No leader change in 5000 ms');
|
||||
}
|
||||
|
||||
async function run()
|
||||
{
|
||||
await testStartThenRemoveNode();
|
||||
await testAddNode();
|
||||
await testPreVote();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue