Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/vote for new epoche #1

Merged
merged 11 commits into from
May 17, 2023
Merged
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Changelog
All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

Please check our [developers guide](https://gitlab.com/tokend/developers-guide)
for further information about branching and tagging conventions.

## [0.1.0-rc.0] - 2022-05-15
#### Added
- Voting for new epoch
- Selecting a new proposer after voting

[0.1.0-rc.0]: https://gitlab.com/tokend/fullerton-staking/fullerton-web-client/tags/1.0.0
4 changes: 1 addition & 3 deletions README
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
super hacky visualization of raft, inspired by thesecretlivesofdata
super hacky visualization of PaLa, inspired by Distributed Lab

also makes for a good space heater while your browser continuously re-parses and re-renders svg



after you clone, run the following to get the dependencies:
git submodule update --init --recursive
4 changes: 2 additions & 2 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Raft Scope</title>
<title>PaLa Scope</title>
<link href="bootstrap-3.1.1/css/bootstrap.min.css" rel="stylesheet">
<link href="bootstrap-slider/css/bootstrap-slider.css" rel="stylesheet">
<script src="jquery/jquery.min.js"></script>
<script src="bootstrap-3.1.1/js/bootstrap.min.js"></script>
<script src="bootstrap-slider/js/bootstrap-slider.js"></script>
<script src="bootstrap-contextmenu/bootstrap-contextmenu.js"></script>
<script src="util.js"></script>
<script src="raft.js"></script>
<script src="pala.js"></script>
<script src="state.js"></script>
<script src="script.js"></script>
<link href="style.css" rel="stylesheet">
Expand Down
175 changes: 96 additions & 79 deletions raft.js → pala.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,17 @@
/* jshint devel: true */
/* jshint jquery: true */
/* global util */
/* global START_PROPOSER_IDX */
'use strict';

const raft = {};
const pala = {};
const RPC_TIMEOUT = 50000;
const MIN_RPC_LATENCY = 10000;
const MIN_COUNT_OF_VOTES = 1
const MAX_RPC_LATENCY = 15000;
const ELECTION_TIMEOUT = 100000;
const NUM_SERVERS = 5;
const NUM_SERVERS = 7;
const BATCH_SIZE = 1;
const MIN_VOTES_AMOUNT_FOR_MAKING_DECISION = Math.floor(NUM_SERVERS * 2 / 3)

const DIRECTIONS = {
request: 'request',
Expand All @@ -28,18 +30,18 @@ const SERVER_STATES = {
const REQUEST_TYPES = {
requestVote: 'RequestVote',
appendEntries: 'AppendEntries',
timeoutMessage: 'TimeoutMessage'
};

(function() {

const rules = {};
raft.rules = rules;
pala.rules = rules;

const sendMessage = (model, message) => {
message.sendTime = model.time;
message.recvTime = model.time +
MIN_RPC_LATENCY +
Math.random() * (MAX_RPC_LATENCY - MIN_RPC_LATENCY);
MAX_RPC_LATENCY
model.messages.push(message);
};

Expand All @@ -49,11 +51,14 @@ const sendRequest = (model, request) => {
};

const sendReply = (model, request, reply) => {
reply.from = request.to;
reply.to = request.from;
reply.type = request.type;
reply.direction = DIRECTIONS.reply;
sendMessage(model, reply);
model.servers.filter(item => item.id !== request.to).forEach(item => {
const newReply = {...reply}
newReply.from = request.to;
newReply.to = item.id;
newReply.type = request.type;
newReply.direction = DIRECTIONS.reply;
sendMessage(model, newReply);
})
};

const logTerm = (log, index) => {
Expand All @@ -63,19 +68,18 @@ const logTerm = (log, index) => {
};

const makeElectionAlarm = (now) => {
return now + (Math.random() + 1) * ELECTION_TIMEOUT;
return now + ELECTION_TIMEOUT;
};

raft.server = (id, peers) => {
pala.server = (id, peers, isLeader) => {
return {
id: id,
peers: peers,
state: SERVER_STATES.follower,
state: isLeader ? SERVER_STATES.leader : SERVER_STATES.follower,
term: 1,
votedFor: null,
log: [],
commitIndex: 0,
electionAlarm: makeElectionAlarm(0),
electionAlarm: isLeader ? 0 : makeElectionAlarm(0),
voteGranted: util.makeMap(peers, false),
matchIndex: util.makeMap(peers, 0),
nextIndex: util.makeMap(peers, 1),
Expand All @@ -84,6 +88,15 @@ raft.server = (id, peers) => {
};
};

const getNewProposerIdxFromServer = (epoch) => epoch % NUM_SERVERS + 1

const getVotesCount = (server) => Object.values(server.voteGranted).reduce((acc, item) => {
if(item) {
acc += 1
}
return acc

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.reduce(
  (acc, item) =>  item ? acc + 1 : acc,
  MIN_COUNT_OF_VOTES
)

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

}, MIN_COUNT_OF_VOTES)

const stepDown = (model, server, term) => {
server.term = term;
server.state = SERVER_STATES.follower;
Expand All @@ -94,17 +107,13 @@ const stepDown = (model, server, term) => {
};

rules.startNewElection = (model, server) => {
if ((server.state === SERVER_STATES.follower || server.state === SERVER_STATES.candidate) &&
server.electionAlarm <= model.time) {
server.electionAlarm = makeElectionAlarm(model.time);
server.term += 1;
const isLeaderExist = model.servers.some(item => item.state === SERVER_STATES.leader)
if ((server.state === SERVER_STATES.follower) &&

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (server.state === SERVER_STATES.follower &&
  server.electionAlarm <= model.time &&
  !isLeaderExist
) {

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

server.electionAlarm <= model.time && !isLeaderExist) {
clearServer(model, server)
server.votedFor = server.id;
server.state = SERVER_STATES.candidate;
server.voteGranted = util.makeMap(server.peers, false);
server.matchIndex = util.makeMap(server.peers, 0);
server.nextIndex = util.makeMap(server.peers, 1);
server.rpcDue = util.makeMap(server.peers, 0);
server.heartbeatDue = util.makeMap(server.peers, 0);
server.rpcDue = util.makeMap(server.peers, model.time)
}
};

Expand All @@ -118,22 +127,37 @@ rules.sendRequestVote = (model, server, peer) => {
type: REQUEST_TYPES.requestVote,
term: server.term,
lastLogTerm: logTerm(server.log, server.log.length),
lastLogIndex: server.log.length});
lastLogIndex: server.log.length
});
}
};

rules.becomeLeader = (model, server) => {
if (server.state === SERVER_STATES.candidate &&
util.countTrue(util.mapValues(server.voteGranted)) + 1 > Math.floor(NUM_SERVERS / 2)) {
//console.log('server ' + server.id + ' is leader in term ' + server.term);
server.state = SERVER_STATES.leader;
server.nextIndex = util.makeMap(server.peers, server.log.length + 1);
server.rpcDue = util.makeMap(server.peers, util.Inf);
server.heartbeatDue = util.makeMap(server.peers, 0);
server.electionAlarm = util.Inf;
const countOfVotes = getVotesCount(server)

if(countOfVotes >= MIN_VOTES_AMOUNT_FOR_MAKING_DECISION && server.state === SERVER_STATES.candidate) {
const proposerIdx = getNewProposerIdxFromServer(server.term)

if(server.id === proposerIdx) {
server.state = SERVER_STATES.leader
server.nextIndex = util.makeMap(server.peers, server.log.length + 1);
server.rpcDue = util.makeMap(server.peers, util.Inf);
server.heartbeatDue = util.makeMap(server.peers, 0);
server.electionAlarm = util.Inf;
}
clearServer(model, server)
server.term += 1
}
};

const clearServer = (model, server) => {
server.votedFor = null
server.electionAlarm = server.state === SERVER_STATES.leader || server.state === SERVER_STATES.stopped ? 0 : makeElectionAlarm(model.time)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

split into a line

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

server.voteGranted = util.makeMap(server.peers, false)
server.rpcDue = util.makeMap(server.peers, model.time + RPC_TIMEOUT)
server.heartbeatDue = util.makeMap(server.peers, model.time + ELECTION_TIMEOUT / 2)
}

rules.sendAppendEntries = (model, server, peer) => {
if (server.state === SERVER_STATES.leader &&
(server.heartbeatDue[peer] <= model.time ||
Expand Down Expand Up @@ -170,28 +194,19 @@ rules.advanceCommitIndex = (model, server) => {
};

const handleRequestVoteRequest = (model, server, request) => {
if (server.term < request.term)
stepDown(model, server, request.term);
let granted = false;
if (server.term === request.term &&
(server.votedFor === null ||
server.votedFor === request.from) &&
(request.lastLogTerm > logTerm(server.log, server.log.length) ||
(request.lastLogTerm === logTerm(server.log, server.log.length) &&
request.lastLogIndex >= server.log.length))) {
granted = true;
if (server.state === SERVER_STATES.candidate) {
server.votedFor = request.from;
server.electionAlarm = makeElectionAlarm(model.time);
server.peers.forEach(peerId => {
server.voteGranted[peerId] =

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const isVoteGranted = server.term === request.term &&
  server.id === request.to &&
  peerId === request.from
if (isVoteGranted) {
  server.voteGranted[peerId] = isVoteGranted
}

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done, but need to talk

(server.term === request.term &&
server.id === request.to &&
peerId === request.from) || server.voteGranted[peerId];
})
}
sendReply(model, request, {
term: server.term,
granted: granted,
});
};

const handleRequestVoteReply = (model, server, reply) => {
if (server.term < reply.term)
stepDown(model, server, reply.term);
if (server.state === SERVER_STATES.candidate &&
server.term === reply.term) {
server.rpcDue[reply.from] = util.Inf;
Expand All @@ -210,7 +225,7 @@ const handleAppendEntriesRequest = (model, server, request) => {
if (request.prevIndex === 0 ||
(request.prevIndex <= server.log.length &&
logTerm(server.log, request.prevIndex) === request.prevTerm)) {
success = true;
success = true;
let index = request.prevIndex;
for (let i = 0; i < request.entries.length; i += 1) {
index += 1;
Expand All @@ -229,6 +244,7 @@ const handleAppendEntriesRequest = (model, server, request) => {
term: server.term,
success: success,
matchIndex: matchIndex,
granted: true
});
};

Expand All @@ -239,7 +255,7 @@ const handleAppendEntriesReply = (model, server, reply) => {
server.term === reply.term) {
if (reply.success) {
server.matchIndex[reply.from] = Math.max(server.matchIndex[reply.from],
reply.matchIndex);
reply.matchIndex);
}
server.nextIndex[reply.from] = reply.success ? reply.matchIndex + 1 : Math.max(1, server.nextIndex[reply.from] - 1);
server.rpcDue[reply.from] = 0;
Expand All @@ -263,7 +279,7 @@ const handleMessage = (model, server, message) => {
};


raft.update = (model) => {
pala.update = (model) => {
model.servers.forEach((server) => {
rules.startNewElection(model, server);
rules.becomeLeader(model, server);
Expand All @@ -290,45 +306,47 @@ raft.update = (model) => {
});
};

raft.stop = (model, server) => {
pala.stop = (model, server) => {
clearServer(model, server)
server.state = SERVER_STATES.stopped;
server.electionAlarm = 0;
};

raft.resume = (model, server) => {
pala.resume = (model, server) => {
clearServer(model, server)
server.state = SERVER_STATES.follower;
server.electionAlarm = makeElectionAlarm(model.time);
};

raft.resumeAll = (model) => {
pala.resumeAll = (model) => {
model.servers.forEach((server) => {
raft.resume(model, server);
pala.resume(model, server);
});
};

raft.restart = (model, server) => {
raft.stop(model, server);
raft.resume(model, server);
pala.restart = (model, server) => {
pala.stop(model, server);
pala.resume(model, server);
};

raft.drop = (model, message) => {
pala.drop = (model, message) => {
model.messages = model.messages.filter((msg) => msg !== message);
};

raft.timeout = (model, server) => {
pala.timeout = (model, server) => {
server.state = SERVER_STATES.follower;
server.electionAlarm = 0;
rules.startNewElection(model, server);
};

raft.clientRequest = (model, server) => {
pala.clientRequest = (model, server) => {
if (server.state === SERVER_STATES.leader) {
server.log.push({term: server.term,
value: 'v'});
}
};

raft.spreadTimers = (model) => {
pala.spreadTimers = (model) => {
const timers = [];
model.servers.forEach((server) => {
if (server.electionAlarm > model.time &&
Expand Down Expand Up @@ -358,8 +376,8 @@ raft.spreadTimers = (model) => {
}
};

raft.alignTimers = (model) => {
raft.spreadTimers(model);
pala.alignTimers = (model) => {
pala.spreadTimers(model);
const timers = [];
model.servers.forEach((server) => {
if (server.electionAlarm > model.time &&
Expand All @@ -376,13 +394,13 @@ raft.alignTimers = (model) => {
});
};

raft.setupLogReplicationScenario = (model) => {
pala.setupLogReplicationScenario = (model) => {
const s1 = model.servers[0];
raft.restart(model, model.servers[1]);
raft.restart(model, model.servers[2]);
raft.restart(model, model.servers[3]);
raft.restart(model, model.servers[4]);
raft.timeout(model, model.servers[0]);
pala.restart(model, model.servers[1]);
pala.restart(model, model.servers[2]);
pala.restart(model, model.servers[3]);
pala.restart(model, model.servers[4]);
pala.timeout(model, model.servers[0]);
rules.startNewElection(model, s1);
model.servers[1].term = 2;
model.servers[2].term = 2;
Expand All @@ -393,13 +411,12 @@ raft.setupLogReplicationScenario = (model) => {
model.servers[3].votedFor = 1;
model.servers[4].votedFor = 1;
s1.voteGranted = util.makeMap(s1.peers, true);
raft.stop(model, model.servers[2]);
raft.stop(model, model.servers[3]);
raft.stop(model, model.servers[4]);
pala.stop(model, model.servers[2]);
pala.stop(model, model.servers[3]);
pala.stop(model, model.servers[4]);
rules.becomeLeader(model, s1);
raft.clientRequest(model, s1);
raft.clientRequest(model, s1);
raft.clientRequest(model, s1);
pala.clientRequest(model, s1);
pala.clientRequest(model, s1);
pala.clientRequest(model, s1);
};

})();
Loading