Compare commits

...

1 Commits

Author SHA1 Message Date
Vitaliy Filippov a0b20be22c Pre-vote protocol (experimental) 2023-06-28 02:06:52 +03:00
2 changed files with 116 additions and 3 deletions

View File

@ -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;

View File

@ -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);
} }