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
|
// The only requirement is to guarantee preservation of entries confirmed by
|
||||||
// all hosts participating in consensus.
|
// 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
|
// https://github.com/eBay/NuRaft/blob/master/docs/leadership_expiration.md
|
||||||
|
|
||||||
const EventEmitter = require('events');
|
const EventEmitter = require('events');
|
||||||
|
@ -18,6 +19,10 @@ const VOTE_REQUEST = 'vote_request';
|
||||||
const VOTE = 'vote';
|
const VOTE = 'vote';
|
||||||
const PING = 'ping';
|
const PING = 'ping';
|
||||||
const PONG = 'pong';
|
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 CANDIDATE = 'candidate';
|
||||||
const LEADER = 'leader';
|
const LEADER = 'leader';
|
||||||
|
@ -31,6 +36,7 @@ class TinyRaft extends EventEmitter
|
||||||
// electionTimeout?: number,
|
// electionTimeout?: number,
|
||||||
// heartbeatTimeout?: number,
|
// heartbeatTimeout?: number,
|
||||||
// leadershipTimeout?: number,
|
// leadershipTimeout?: number,
|
||||||
|
// enablePrevote?: bool,
|
||||||
// initialTerm?: number,
|
// initialTerm?: number,
|
||||||
// }
|
// }
|
||||||
constructor(config)
|
constructor(config)
|
||||||
|
@ -43,6 +49,7 @@ class TinyRaft extends EventEmitter
|
||||||
this.randomTimeout = config.randomTimeout > 0 ? Number(config.randomTimeout) : this.electionTimeout;
|
this.randomTimeout = config.randomTimeout > 0 ? Number(config.randomTimeout) : this.electionTimeout;
|
||||||
this.heartbeatTimeout = Number(config.heartbeatTimeout) || 1000;
|
this.heartbeatTimeout = Number(config.heartbeatTimeout) || 1000;
|
||||||
this.leadershipTimeout = Number(config.leadershipTimeout) || 0;
|
this.leadershipTimeout = Number(config.leadershipTimeout) || 0;
|
||||||
|
this.enablePrevote = config.enablePrevote ? true : false;
|
||||||
if (!this.nodeId || this.nodeId instanceof Object ||
|
if (!this.nodeId || this.nodeId instanceof Object ||
|
||||||
!(this.nodes instanceof Array) || this.nodes.filter(n => !n || n instanceof Object).length > 0 ||
|
!(this.nodes instanceof Array) || this.nodes.filter(n => !n || n instanceof Object).length > 0 ||
|
||||||
!(this.send instanceof Function))
|
!(this.send instanceof Function))
|
||||||
|
@ -52,6 +59,7 @@ class TinyRaft extends EventEmitter
|
||||||
this.term = 0;
|
this.term = 0;
|
||||||
this.state = null;
|
this.state = null;
|
||||||
this.leader = null;
|
this.leader = null;
|
||||||
|
this.preVoting = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
_nextTerm(after)
|
_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()
|
start()
|
||||||
{
|
{
|
||||||
this._nextTerm(-1);
|
this._nextTerm(-1);
|
||||||
this.electionTimer = null;
|
this.electionTimer = null;
|
||||||
|
if (this.enablePrevote && !this.preVoting)
|
||||||
|
{
|
||||||
|
this._prevote();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.preVoting = false;
|
||||||
this.term++;
|
this.term++;
|
||||||
this.voted = 1;
|
this.voted = 1;
|
||||||
this.votes = { [this.nodeId]: [ this.nodeId ] };
|
this.votes = { [this.nodeId]: [ this.nodeId ] };
|
||||||
|
@ -123,7 +153,36 @@ class TinyRaft extends EventEmitter
|
||||||
|
|
||||||
onReceive(from, msg)
|
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)
|
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_REQUEST = VOTE_REQUEST;
|
||||||
TinyRaft.VOTE = VOTE;
|
TinyRaft.VOTE = VOTE;
|
||||||
TinyRaft.PING = PING;
|
TinyRaft.PING = PING;
|
||||||
|
|
|
@ -10,7 +10,7 @@ function newNode(id, nodes, partitions)
|
||||||
electionTimeout: 500,
|
electionTimeout: 500,
|
||||||
send: function(to, msg)
|
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));
|
console.log('received from '+n.nodeId+' to '+to+': '+JSON.stringify(msg));
|
||||||
setImmediate(function() { nodes[to].onReceive(n.nodeId, msg); });
|
setImmediate(function() { nodes[to].onReceive(n.nodeId, msg); });
|
||||||
|
@ -148,10 +148,62 @@ async function testAddNode()
|
||||||
console.log('testAddNode: OK');
|
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()
|
async function run()
|
||||||
{
|
{
|
||||||
await testStartThenRemoveNode();
|
await testStartThenRemoveNode();
|
||||||
await testAddNode();
|
await testAddNode();
|
||||||
|
await testPreVote();
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue