diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..1a49a71
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,16 @@
+# 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
+- Recovery flow
+- 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
diff --git a/README b/README
index d987e5b..a831fec 100644
--- a/README
+++ b/README
@@ -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
diff --git a/index.html b/index.html
index 5f8d35e..d891e6e 100644
--- a/index.html
+++ b/index.html
@@ -3,7 +3,7 @@
- Raft Scope
+ PaLa Scope
@@ -11,7 +11,7 @@
-
+
diff --git a/pala.js b/pala.js
new file mode 100644
index 0000000..39f91b5
--- /dev/null
+++ b/pala.js
@@ -0,0 +1,721 @@
+/* jshint globalstrict: true */
+/* jshint browser: true */
+/* jshint devel: true */
+/* jshint jquery: true */
+/* global util */
+/* global START_PROPOSER_IDX */
+'use strict';
+
+const pala = {};
+const RPC_TIMEOUT = 100000;
+const MIN_COUNT_OF_VOTES = 1
+const FIRST_NODE_IDX = 1
+const RPC_LATENCY = 10000;
+const ELECTION_TIMEOUT = 150000;
+const NUM_SERVERS = 7;
+const BATCH_SIZE = 1;
+const MIN_VOTES_AMOUNT_FOR_MAKING_DECISION = Math.ceil(NUM_SERVERS * 2 / 3)
+const K_BLOCKS = 2
+
+const MESSAGE_DIRECTIONS = {
+ request: 'request',
+ reply: 'reply',
+};
+
+const SERVER_STATES = {
+ follower: 'follower',
+ leader: 'leader',
+ stopped: 'stopped',
+ candidate: 'candidate',
+ recovery: 'recovery'
+};
+
+const REQUEST_TYPES = {
+ requestVote: 'RequestVote',
+ appendEntries: 'AppendEntries',
+ recoveryMessage: 'RecoveryMessage',
+ epochChanging: 'EpochChanging'
+};
+
+(function() {
+
+const rules = {};
+pala.rules = rules;
+
+const sendMessage = (model, message) => {
+ message.sendTime = model.time;
+ message.recvTime = model.time + RPC_LATENCY
+ model.messages.push(message);
+};
+
+const sendRequest = (model, request) => {
+ request.direction = MESSAGE_DIRECTIONS.request;
+ sendMessage(model, request);
+};
+
+const sendReply = (model, messageTo, request, reply) => {
+ const newReply = {...reply}
+ newReply.from = request.to;
+ newReply.to = messageTo;
+ newReply.type = request.type;
+ newReply.direction = MESSAGE_DIRECTIONS.reply;
+ newReply.blockToVote = reply?.blockToVote
+ newReply.replyBlock = reply?.replyBlock
+ sendMessage(model, newReply);
+}
+
+const sendMultiReply = (model, request, reply) => {
+ model.servers.filter(item => item.id !== request.to).forEach(item => {
+ sendReply(model, item.id, request, reply)
+ })
+};
+
+const logTerm = (log, index) => {
+ return index < 1 || index > log.length
+ ? 0
+ : log[index - 1].epoch;
+};
+
+const makeElectionAlarm = (now) => {
+ return now + ELECTION_TIMEOUT;
+};
+
+const isRecoveryEnded = (server) => {
+ return Object.values(server.recoveryPaths).reduce((acc, item) => {
+ if(item) acc += 1
+ return acc
+ }, MIN_COUNT_OF_VOTES) === NUM_SERVERS
+}
+
+const getNextServerIdx = id => id === NUM_SERVERS ? FIRST_NODE_IDX : (id + 1);
+
+const getNewProposerIdxFromServer = (epoch) => epoch ? epoch % NUM_SERVERS : FIRST_NODE_IDX
+
+const getVotesCount = (server) => Object.values(server.voteGranted).reduce((acc, item) => {
+ if(item) {
+ acc += 1
+ }
+ return acc
+}, MIN_COUNT_OF_VOTES)
+
+const countVotesForBlock = (server, isRequest) => Object.values(server.votesForBlock).reduce((acc, item) => {
+ if(isRequest ? server.blockToVote >= item : item === server.blockToVote) {
+ acc += 1
+ }
+ return acc
+ }, server.state !== SERVER_STATES.leader
+ ? MIN_COUNT_OF_VOTES + 1
+ : MIN_COUNT_OF_VOTES
+)
+
+pala.server = (id, peers, isLeader) => {
+ return {
+ id: id,
+ peers: peers,
+ state: isLeader ? SERVER_STATES.leader : SERVER_STATES.follower,
+ epoch: 1,
+ votedFor: null,
+ log: [],
+ isLeaderPrevState: isLeader,
+ blockHistory: [],
+ blockToVote: 0,
+ recoveryTimeout: 0,
+ commitIndex: 0,
+ lastAddedBlockDuringRecovery: -1,
+ lastAddedLogDuringRecovery: [],
+ isRecoveryEndedForCurrentServer: false,
+ isRecoveryMessageSent: false,
+ isRequestWasSent: false,
+ epochChangingMessage: { isSent: false, isCanBeSent: true, epochToChange: 0 },
+ currentRecoveryId: getNextServerIdx(id),
+ recoveryPaths: util.makeMap(peers, false),
+ electionAlarm: makeElectionAlarm(0),
+ votesForBlock: util.makeMap(peers, -1),
+ voteGranted: util.makeMap(peers, false),
+ matchIndex: util.makeMap(peers, 0),
+ nextIndex: util.makeMap(peers, 1),
+ rpcDue: util.makeMap(peers, 0),
+ heartbeatDue: util.makeMap(peers, 0),
+ };
+};
+
+const onEndOfRecovery = (model, server) => {
+ server.isRecoveryEndedForCurrentServer = false
+ server.isRecoveryMessageSent = false
+ server.currentRecoveryId = getNextServerIdx(server.id)
+ server.votesForBlock = util.makeMap(server.peers, -1)
+ server.blockToVote = Math.max(server.blockToVote, server.blockHistory[server.blockHistory.length - 1].block) + 1
+ if(server.lastAddedBlockDuringRecovery >= 0) {
+ if(server.blockHistory[server.blockHistory.length - 1]?.block !== server.lastAddedBlockDuringRecovery) {
+ server.blockHistory.push({ block: server.lastAddedBlockDuringRecovery, epoch: server.epoch, log: [...server.lastAddedLogDuringRecovery] })
+ }
+ server.lastAddedBlockDuringRecovery = -1
+ server.lastAddedLogDuringRecovery = []
+ }
+ clearServer(model, server)
+}
+
+rules.startNewElection = (model, server) => {
+ if ((server.state === SERVER_STATES.follower || server.state === SERVER_STATES.leader) &&
+ server.electionAlarm <= model.time) {
+ clearServer(model, server)
+ server.votedFor = server.id;
+ server.state = SERVER_STATES.candidate;
+ server.rpcDue = util.makeMap(server.peers, model.time)
+ }
+ if(server.state === SERVER_STATES.recovery && server.electionAlarm <= model.time) {
+ if(server.isRecoveryMessageSent) {
+ server.isRecoveryEndedForCurrentServer = true
+ server.recoveryPaths[server.currentRecoveryId] = true
+ server.isRecoveryMessageSent = false
+ if(isRecoveryEnded(server)) {
+ onEndOfRecovery(model, server)
+ return
+ }
+ const nextRecoveryIndex = getNextServerIdx(server.currentRecoveryId)
+ server.currentRecoveryId = nextRecoveryIndex === server.id ? getNextServerIdx(server.id) : nextRecoveryIndex
+ return
+ }
+ if(server.isRecoveryEndedForCurrentServer) {
+ if(isRecoveryEnded(server)) {
+ onEndOfRecovery(model, server)
+ return
+ }
+ server.isRecoveryEndedForCurrentServer = false
+ const nextRecoveryIndex = getNextServerIdx(server.currentRecoveryId)
+ server.currentRecoveryId = nextRecoveryIndex === server.id ? getNextServerIdx(server.id) : nextRecoveryIndex
+ }
+ }
+};
+
+rules.sendRequestVote = (model, server, peer) => {
+ if (server.state === SERVER_STATES.candidate &&
+ server.rpcDue[peer] <= model.time) {
+ server.rpcDue[peer] = model.time + RPC_TIMEOUT;
+ sendRequest(model, {
+ from: server.id,
+ to: peer,
+ type: REQUEST_TYPES.requestVote,
+ epoch: server.epoch,
+ lastLogTerm: logTerm(server.log, server.log.length),
+ lastLogIndex: server.log.length
+ });
+ }
+};
+
+rules.becomeLeader = (model, server) => {
+ server.epochChangingMessage.isSent = false
+ if(isRecoveryEnded(server)) {
+ const countOfVotes = getVotesCount(server)
+ server.voteGranted = util.makeMap(server.peers, false)
+ server.recoveryPaths = util.makeMap(server.peers, false)
+ server.isRecoveryEndedForCurrentServer = false
+ if(countOfVotes >= MIN_VOTES_AMOUNT_FOR_MAKING_DECISION) {
+ server.epochChangingMessage.isSent = true
+ server.epoch += 1
+ server.epochChangingMessage.isCanBeSent = true
+ server.state = server.id === getNewProposerIdxFromServer(server.epoch) ? SERVER_STATES.leader : SERVER_STATES.follower
+ clearServer(model, server)
+ return
+ }
+ if(!server.epochChangingMessage.isCanBeSent) {
+ server.epochChangingMessage.isCanBeSent = false
+ server.epoch = server.epochChangingMessage.serverEpochToChange
+ }
+ server.state = server.id === getNewProposerIdxFromServer(server.epoch) ? SERVER_STATES.leader : SERVER_STATES.follower
+ clearServer(model, server)
+ }
+};
+
+const clearServer = (model, server) => {
+ server.isRecoveryMessageSent = false
+ server.votedFor = null
+ server.electionAlarm = server.state === SERVER_STATES.stopped ? 0 : makeElectionAlarm(model.time)
+ server.rpcDue = util.makeMap(server.peers, model.time + RPC_TIMEOUT)
+ server.heartbeatDue = util.makeMap(server.peers, 0)
+}
+
+rules.sendRecoveryRequest = (model, server) => {
+ if(server.state === SERVER_STATES.recovery && server.electionAlarm <= model.time) {
+ const lastBlock = server.blockHistory[server.blockHistory.length - 1]?.block
+ sendRequest(model, {
+ type: REQUEST_TYPES.recoveryMessage,
+ from: server.id,
+ to: server.currentRecoveryId,
+ epoch: server.epoch,
+ neededBlock: Number.isInteger(lastBlock) ? lastBlock + 1 : 0,
+ }, true);
+ server.isRecoveryMessageSent = true
+ server.electionAlarm = makeElectionAlarm( model.time - ELECTION_TIMEOUT / 2)
+ }
+}
+
+const handleRecoveryRequest = (model, server, request) => {
+ if(server.state === SERVER_STATES.stopped) {
+ const requestor = model.servers.find(({id}) => id === request.from)
+ requestor.recoveryPaths[server.id] = true
+ requestor.isRecoveryMessageSent = false
+ requestor.isRecoveryEndedForCurrentServer = true
+ requestor.electionAlarm = 0
+ return
+ }
+ sendReply(model, request.from, request, {
+ epoch: server.epoch,
+ success: true,
+ granted: true,
+ replyBlock: server.blockHistory.find(item => item.block === request.neededBlock),
+ }, true);
+}
+
+const handleRecoveryReply = (model, server, reply) => {
+ if(server.state === SERVER_STATES.recovery) {
+ server.isRecoveryMessageSent = false
+ server.recoveryPaths[server.currentRecoveryId] = true
+ server.electionAlarm = 0
+ if(reply.replyBlock) {
+ server.epoch = server.epoch > reply.replyBlock.epoch ? server.epoch : reply.replyBlock.epoch
+ server.blockHistory.push(reply.replyBlock)
+ server.isRecoveryEndedForCurrentServer = false
+ server.blockToVote = server.blockHistory[server.blockHistory.length - 1].block + 1
+ server.commitIndex = reply.replyBlock.commitIndex
+ server.log = reply.replyBlock.fullLog
+ model.servers.forEach(serv => {
+ if(serv.state !== SERVER_STATES.stopped) {
+ serv.nextIndex[server.id] = server.commitIndex + 1
+ serv.matchIndex[server.id] = server.commitIndex
+ }
+ })
+ return
+ }
+ server.isRecoveryEndedForCurrentServer = true
+ }
+}
+
+rules.sendEpochChangingRequest = (model, server, recipientId) => {
+ if(
+ server.epochChangingMessage.isSent &&
+ server.epochChangingMessage.isCanBeSent &&
+ server.state !== SERVER_STATES.stopped &&
+ server.state !== SERVER_STATES.recovery
+ ) {
+ sendRequest(model, {
+ type: REQUEST_TYPES.epochChanging,
+ from: server.id,
+ to: recipientId,
+ epoch: server.epoch,
+ }, true);
+ }
+}
+
+const handleEpochChangingRequest = (model, server, message) => {
+ server.voteGranted = util.makeMap(server.peers, false)
+ if(server.epoch < message.epoch) {
+ if(server.state !== SERVER_STATES.stopped && server.state !== SERVER_STATES.recovery) {
+ server.epoch = message.epoch
+ const nextLeaderId = getNewProposerIdxFromServer(server.epoch)
+ server.state = nextLeaderId === server.id ? SERVER_STATES.leader : SERVER_STATES.follower
+ server.electionAlarm = makeElectionAlarm(model.time)
+ }
+ if(server.state === SERVER_STATES.recovery) {
+ server.epochChangingMessage.serverEpochToChange = message.epoch
+ server.epochChangingMessage.isCanBeSent = false
+ }
+ }
+}
+
+const handleRequestVoteRequest = (model, server, request) => {
+ if(server.state !== SERVER_STATES.stopped) {
+ server.votedFor = request.from;
+ server.peers.forEach(peerId => {
+ server.voteGranted[peerId] =
+ (server.epoch === request.epoch &&
+ server.id === request.to && peerId === request.from)
+ || server.voteGranted[peerId];
+ })
+ }
+};
+
+const handleRequestVoteReply = (model, server, reply) => {
+ if (server.state === SERVER_STATES.candidate &&
+ server.epoch === reply.epoch) {
+ server.rpcDue[reply.from] = util.Inf;
+ server.voteGranted[reply.from] = reply.granted;
+ }
+};
+
+const createBlockHistory = (model, serverId) => {
+ const foundServer = model.servers.find(({id}) => id === serverId)
+ const foundLog = foundServer.log.find(servLog => servLog.neededBlockNumber === foundServer.blockToVote)
+ if(
+ (foundServer.state !== SERVER_STATES.stopped && foundServer.state !== SERVER_STATES.recovery) ||
+ (foundServer.state === SERVER_STATES.recovery && isRecoveryEnded(foundServer))
+ ) {
+ foundServer.blockHistory.push({
+ block: foundServer.blockToVote,
+ epoch: foundServer.epoch,
+ nextIndex: {...foundServer.nextIndex},
+ matchIndex: {...foundServer.matchIndex},
+ commitIndex: foundServer.commitIndex,
+ log: (foundLog ? [foundLog] : []),
+ fullLog: [...foundServer.log],
+ isNotarized: false,
+ })
+ }
+}
+
+rules.sendAppendEntries = (model, server) => {
+ server.peers.forEach((peer, idx) => {
+ if(server.state === SERVER_STATES.leader) {
+ if ((server.heartbeatDue[peer] <= model.time)) {
+ const prevIndex = server.nextIndex[peer] - 1;
+ let lastIndex = Math.max(prevIndex + BATCH_SIZE,
+ server.log.length);
+ if (server.matchIndex[peer] + 1 < server.nextIndex[peer])
+ lastIndex = prevIndex;
+ server.votesForBlock = util.makeMap(server.peers, -1)
+ sendRequest(model, {
+ from: server.id,
+ to: peer,
+ type: REQUEST_TYPES.appendEntries,
+ epoch: server.epoch,
+ prevIndex: prevIndex,
+ blockToVote: server.blockToVote,
+ prevTerm: logTerm(server.log, prevIndex),
+ entries: server.log.slice(prevIndex, lastIndex),
+ fullEntries: server.log,
+ commitIndex: Math.min(server.commitIndex, lastIndex)
+ });
+ server.rpcDue[peer] = model.time + RPC_TIMEOUT;
+ server.heartbeatDue[peer] = model.time + ELECTION_TIMEOUT / 2;
+ const foundServer = model.servers.find(({id}) => id === peer)
+ const lastBlockIdx = foundServer.blockHistory[foundServer.blockHistory.length - 1]?.block
+ foundServer.blockToVote = lastBlockIdx ? lastBlockIdx + 1 : server.blockToVote + 1
+ createBlockHistory(model, peer)
+ if(server.peers.length - 1 === idx) {
+ const lastProposerBlockIdx = server.blockHistory[server.blockHistory.length - 1]?.block
+ server.blockToVote = lastProposerBlockIdx ? lastProposerBlockIdx + 1 : server.blockToVote + 1
+ createBlockHistory(model, server.id)
+ }
+ }
+ }
+ })
+};
+
+const handleAppendEntriesRequest = (model, server, request) => {
+ let success = false;
+ let matchIndex = 0;
+ if (server.epoch === request.epoch && request.blockToVote - server.blockToVote <= K_BLOCKS) {
+ const lastBlockIdx = server.blockHistory[server.blockHistory.length - 1]?.block
+ server.blockToVote = Math.max(lastBlockIdx, server.blockToVote) + 1
+ if (request.prevIndex === 0 ||
+ (request.prevIndex <= server.log.length &&
+ logTerm(server.log, request.prevIndex) === request.prevTerm)) {
+ success = true;
+ let index = request.prevIndex;
+ for (let i = 0; i < request.entries.length; i += 1) {
+ index += 1;
+ if (logTerm(server.log, index) !== request.entries[i].epoch) {
+ while (server.log.length > index - 1)
+ server.log.pop();
+ server.log.push(request.entries[i]);
+ }
+ }
+ matchIndex = index
+ }
+ if(server.state === SERVER_STATES.recovery) return
+ sendMultiReply(model, request, {
+ entries: request.entries,
+ epoch: server.epoch,
+ success: success,
+ matchIndex,
+ granted: true,
+ blockToVote: server.blockToVote,
+ });
+ }
+};
+
+const createBlocksTable = (model, server, reply) => {
+ if(server.state === SERVER_STATES.leader) {
+ const notarizedBlocksHistory = server.blockHistory.filter(item => !item.isFinalized)
+ const lastKBlock = server.blockHistory.slice(-K_BLOCKS - 1) ?? []
+ const finalizingCandidate = lastKBlock[0]
+ const kBlocksAfterCandidate = lastKBlock.slice(-K_BLOCKS).filter(({block}) => block !== finalizingCandidate.block)
+ if(finalizingCandidate.isFinalized || !lastKBlock.length) return;
+ const isRequestBlockFinalized =
+ kBlocksAfterCandidate.length === K_BLOCKS &&
+ notarizedBlocksHistory.length > K_BLOCKS &&
+ kBlocksAfterCandidate.every(block => block.isNotarized)
+ if(server.state !== SERVER_STATES.stopped &&
+ server.state !== SERVER_STATES.recovery && isRequestBlockFinalized) {
+ finalizingCandidate.isFinalized = true
+ if(!finalizingCandidate.log.length) return;
+ server.commitIndex += 1
+ const activePeers = server.peers.filter(servId => model.servers.find(item => item.id === servId)?.state !== SERVER_STATES.stopped)
+ activePeers.forEach(peer => {
+ const countedMatchIndex = Math.max(server.matchIndex[peer],
+ reply.matchIndex)
+ server.matchIndex[peer] = reply.matchIndex - server.matchIndex[peer] > 1 ? server.matchIndex[peer] + 1 : countedMatchIndex
+ const peerServ = model.servers.find(({id}) => id === peer)
+ const serversNotPeer = model.servers.filter(serv => serv.id !== peer && serv.state !== SERVER_STATES.leader)
+ peerServ.commitIndex += 1
+ serversNotPeer.forEach(noPeer => {
+ noPeer.matchIndex[peer] = server.matchIndex[peer]
+ noPeer.matchIndex[server.id] = server.matchIndex[peer]
+ })
+ })
+ }
+ }
+}
+
+const handleAppendEntriesReply = (model, server, reply) => {
+ if((server.epoch === reply.epoch && (reply.blockToVote - server.blockToVote <= K_BLOCKS || reply.entries.length)) || server.state === SERVER_STATES.recovery) {
+ server.blockToVote = reply.blockToVote
+ server.votesForBlock[reply.from] = reply.blockToVote
+ const isBlockNotarized = countVotesForBlock(server, reply.entries.length) >= MIN_VOTES_AMOUNT_FOR_MAKING_DECISION
+ if(server.state === SERVER_STATES.recovery && isBlockNotarized) {
+ server.lastAddedBlockDuringRecovery = reply.blockToVote
+ server.lastAddedLogDuringRecovery = reply.entries.length ? reply.entries : []
+ return
+ }
+ if(isBlockNotarized) {
+ notarizeBlock(model, server, reply.blockToVote)
+ }
+ if (server.state === SERVER_STATES.leader && reply.success && isBlockNotarized) {
+ server.electionAlarm = makeElectionAlarm(model.time)
+ server.rpcDue = util.makeMap(server.peers, 0)
+ const activePeers = server.peers.filter(servId => {
+ const foundServer = model.servers.find(item => item.id === servId)
+ return foundServer?.state !== SERVER_STATES.stopped &&
+ foundServer?.state !== SERVER_STATES.recovery &&
+ server.blockToVote - foundServer.blockToVote <= 1
+ } )
+ activePeers.forEach(peer => {
+ server.nextIndex[peer] = Math.max(server.matchIndex[peer], reply.matchIndex + 1)
+ })
+ }
+ if(server.state === SERVER_STATES.follower && isBlockNotarized) {
+ server.electionAlarm = makeElectionAlarm(model.time)
+ }
+ if(isBlockNotarized) {
+ createBlocksTable(model, server, reply)
+ }
+ }
+};
+
+const notarizeBlock = (model, server, blockToVote) => {
+ if(server.state !== SERVER_STATES.recovery && server.state !== SERVER_STATES.stopped) {
+ if(server.blockHistory.find(item => item.block === blockToVote - 1)?.isNotarized) {
+ server.votesForBlock = util.makeMap(server.peers, -1)
+ return
+ }
+ const foundBlocks = server.blockHistory.filter(block => block.block <= blockToVote - 1)
+ foundBlocks.forEach(item => item.isNotarized = true)
+ }
+}
+
+const handleMessage = (model, server, message) => {
+ if (message.type === REQUEST_TYPES.recoveryMessage) {
+ message.direction === MESSAGE_DIRECTIONS.request
+ ? handleRecoveryRequest(model, server, message)
+ : handleRecoveryReply(model, server, message)
+ }
+ if (server.state === SERVER_STATES.stopped)
+ return
+ if (message.type === REQUEST_TYPES.requestVote) {
+ message.direction === MESSAGE_DIRECTIONS.request
+ ? handleRequestVoteRequest(model, server, message)
+ : handleRequestVoteReply(model, server, message);
+ return
+ }
+ if (message.type === REQUEST_TYPES.appendEntries) {
+ message.direction === MESSAGE_DIRECTIONS.request
+ ? handleAppendEntriesRequest(model, server, message)
+ : handleAppendEntriesReply(model, server, message)
+ }
+ if(message.type === REQUEST_TYPES.epochChanging) {
+ handleEpochChangingRequest(model, server, message)
+ }
+};
+
+pala.update = (model) => {
+ model.servers.forEach((server) => {
+ rules.startNewElection(model, server);
+ rules.becomeLeader(model, server);
+ server.peers.forEach((peer) => {
+ rules.sendEpochChangingRequest(model, server, peer)
+ })
+ rules.sendAppendEntries(model, server);
+ rules.sendRecoveryRequest(model, server)
+ server.peers.forEach((peer) => {
+ rules.sendRequestVote(model, server, peer);
+ });
+ if (server.state === SERVER_STATES.candidate) {
+ server.state = SERVER_STATES.recovery
+ server.recoveryPaths = util.makeMap(server.peers, false)
+ server.electionAlarm = 0
+ }
+ });
+ const deliver = [];
+ const keep = [];
+ model.messages.forEach((message) => {
+ message.recvTime <= model.time
+ ? deliver.push(message)
+ : keep.push(message)
+ });
+ model.messages = keep;
+ deliver.forEach((message) => {
+ model.servers.forEach((server) => {
+ if (server.id === message.to) {
+ handleMessage(model, server, message);
+ }
+ });
+ });
+};
+
+pala.stop = (model, server) => {
+ clearServer(model, server)
+ server.isLeaderPrevState = server.state === SERVER_STATES.leader
+ server.state = SERVER_STATES.stopped;
+ server.electionAlarm = 0;
+};
+
+pala.resume = (model, server) => {
+ clearServer(model, server)
+ server.state = server.isLeaderPrevState ? SERVER_STATES.leader : SERVER_STATES.follower;
+ server.electionAlarm = makeElectionAlarm(model.time);
+};
+
+pala.resumeAll = (model) => {
+ model.servers.forEach((server) => {
+ pala.resume(model, server);
+ });
+};
+
+pala.restart = (model, server) => {
+ pala.stop(model, server);
+ pala.resume(model, server);
+};
+
+pala.drop = (model, message) => {
+ model.messages = model.messages.filter((msg) => msg !== message);
+};
+
+pala.timeout = (model, server) => {
+ server.state = SERVER_STATES.follower;
+ server.electionAlarm = 0;
+ rules.startNewElection(model, server);
+};
+
+pala.clientRequest = (model, server) => {
+ if (server.state === SERVER_STATES.leader) {
+ server.heartbeatDue = util.makeMap(server.peers, 0)
+ const lastBlockNum = server.blockHistory[server.blockHistory.length - 1]?.block ?? 0
+ const lastServerLogMessage = server.log[server.log.length - 1]?.neededBlockNumber ?? 0
+ const maxServerIdx = Math.max(...Object.values(server.nextIndex))
+ const activePeers = server.peers.filter(peer => {
+ const foundPeerServer = model.servers.find(({id}) => id === peer && id !== server.id)
+ return foundPeerServer?.state !== SERVER_STATES.stopped && foundPeerServer?.state !== SERVER_STATES.recovery
+ })
+ activePeers.forEach(peer => {
+ server.nextIndex[peer] = maxServerIdx
+ })
+ server.peers.forEach(peer => {
+ const foundServer = model.servers.find(({id}) => peer === id)
+ if(foundServer?.state !== SERVER_STATES.stopped && foundServer?.state !== SERVER_STATES.recovery && server.id !== foundServer?.id) {
+ foundServer.log.push({
+ epoch: server.epoch,
+ value: 'v',
+ neededBlockNumber: (lastBlockNum > lastServerLogMessage ? lastBlockNum : lastServerLogMessage) + 1
+ });
+ const foundServerActivePeers = foundServer.peers.filter(peer => {
+ const foundPeerServer = model.servers.find(({id}) => id === peer && id !== server.id)
+ return foundPeerServer?.state !== SERVER_STATES.stopped && foundPeerServer?.state !== SERVER_STATES.recovery
+ })
+ foundServerActivePeers.forEach(peer => {
+ foundServer.nextIndex[peer] = maxServerIdx
+ })
+ }
+ })
+ server.log.push({
+ epoch: server.epoch,
+ value: 'v',
+ neededBlockNumber: (lastBlockNum > lastServerLogMessage ? lastBlockNum : lastServerLogMessage) + 1
+ });
+ model.servers.forEach(item => item.isRequestWasSent = true)
+ }
+};
+
+pala.spreadTimers = (model) => {
+ const timers = [];
+ model.servers.forEach((server) => {
+ if (server.electionAlarm > model.time &&
+ server.electionAlarm < util.Inf) {
+ timers.push(server.electionAlarm);
+ }
+ });
+ timers.sort(util.numericCompare);
+ if (timers.length > 1 &&
+ timers[1] - timers[0] < RPC_LATENCY) {
+ if (timers[0] > model.time + RPC_LATENCY) {
+ model.servers.forEach((server) => {
+ if (server.electionAlarm === timers[0]) {
+ server.electionAlarm -= RPC_LATENCY;
+ console.log('adjusted S' + server.id + ' timeout forward');
+ }
+ });
+ return
+ }
+ model.servers.forEach((server) => {
+ if (server.electionAlarm > timers[0] &&
+ server.electionAlarm < timers[0] + RPC_LATENCY) {
+ server.electionAlarm += RPC_LATENCY;
+ console.log('adjusted S' + server.id + ' timeout backward');
+ }
+ });
+ }
+};
+
+pala.alignTimers = (model) => {
+ pala.spreadTimers(model);
+ const timers = [];
+ model.servers.forEach((server) => {
+ if (server.electionAlarm > model.time &&
+ server.electionAlarm < util.Inf) {
+ timers.push(server.electionAlarm);
+ }
+ });
+ timers.sort(util.numericCompare);
+ model.servers.forEach((server) => {
+ if (server.electionAlarm === timers[1]) {
+ server.electionAlarm = timers[0];
+ console.log('adjusted S' + server.id + ' timeout forward');
+ }
+ });
+};
+
+pala.setupLogReplicationScenario = (model) => {
+ const s1 = 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].epoch = 2;
+ model.servers[2].epoch = 2;
+ model.servers[3].epoch = 2;
+ model.servers[4].epoch = 2;
+ model.servers[1].votedFor = 1;
+ model.servers[2].votedFor = 1;
+ model.servers[3].votedFor = 1;
+ model.servers[4].votedFor = 1;
+ s1.voteGranted = util.makeMap(s1.peers, true);
+ pala.stop(model, model.servers[2]);
+ pala.stop(model, model.servers[3]);
+ pala.stop(model, model.servers[4]);
+ rules.becomeLeader(model, s1);
+ pala.clientRequest(model, s1);
+ pala.clientRequest(model, s1);
+ pala.clientRequest(model, s1);
+};
+})();
diff --git a/raft.js b/raft.js
deleted file mode 100644
index 7cb9051..0000000
--- a/raft.js
+++ /dev/null
@@ -1,394 +0,0 @@
-/* jshint globalstrict: true */
-/* jshint browser: true */
-/* jshint devel: true */
-/* jshint jquery: true */
-/* global util */
-'use strict';
-
-var raft = {};
-var RPC_TIMEOUT = 50000;
-var MIN_RPC_LATENCY = 10000;
-var MAX_RPC_LATENCY = 15000;
-var ELECTION_TIMEOUT = 100000;
-var NUM_SERVERS = 5;
-var BATCH_SIZE = 1;
-
-(function() {
-
-var sendMessage = function(model, message) {
- message.sendTime = model.time;
- message.recvTime = model.time +
- MIN_RPC_LATENCY +
- Math.random() * (MAX_RPC_LATENCY - MIN_RPC_LATENCY);
- model.messages.push(message);
-};
-
-var sendRequest = function(model, request) {
- request.direction = 'request';
- sendMessage(model, request);
-};
-
-var sendReply = function(model, request, reply) {
- reply.from = request.to;
- reply.to = request.from;
- reply.type = request.type;
- reply.direction = 'reply';
- sendMessage(model, reply);
-};
-
-var logTerm = function(log, index) {
- if (index < 1 || index > log.length) {
- return 0;
- } else {
- return log[index - 1].term;
- }
-};
-
-var rules = {};
-raft.rules = rules;
-
-var makeElectionAlarm = function(now) {
- return now + (Math.random() + 1) * ELECTION_TIMEOUT;
-};
-
-raft.server = function(id, peers) {
- return {
- id: id,
- peers: peers,
- state: 'follower',
- term: 1,
- votedFor: null,
- log: [],
- commitIndex: 0,
- electionAlarm: makeElectionAlarm(0),
- voteGranted: util.makeMap(peers, false),
- matchIndex: util.makeMap(peers, 0),
- nextIndex: util.makeMap(peers, 1),
- rpcDue: util.makeMap(peers, 0),
- heartbeatDue: util.makeMap(peers, 0),
- };
-};
-
-var stepDown = function(model, server, term) {
- server.term = term;
- server.state = 'follower';
- server.votedFor = null;
- if (server.electionAlarm <= model.time || server.electionAlarm == util.Inf) {
- server.electionAlarm = makeElectionAlarm(model.time);
- }
-};
-
-rules.startNewElection = function(model, server) {
- if ((server.state == 'follower' || server.state == 'candidate') &&
- server.electionAlarm <= model.time) {
- server.electionAlarm = makeElectionAlarm(model.time);
- server.term += 1;
- server.votedFor = server.id;
- server.state = '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);
- }
-};
-
-rules.sendRequestVote = function(model, server, peer) {
- if (server.state == 'candidate' &&
- server.rpcDue[peer] <= model.time) {
- server.rpcDue[peer] = model.time + RPC_TIMEOUT;
- sendRequest(model, {
- from: server.id,
- to: peer,
- type: 'RequestVote',
- term: server.term,
- lastLogTerm: logTerm(server.log, server.log.length),
- lastLogIndex: server.log.length});
- }
-};
-
-rules.becomeLeader = function(model, server) {
- if (server.state == '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 = '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;
- }
-};
-
-rules.sendAppendEntries = function(model, server, peer) {
- if (server.state == 'leader' &&
- (server.heartbeatDue[peer] <= model.time ||
- (server.nextIndex[peer] <= server.log.length &&
- server.rpcDue[peer] <= model.time))) {
- var prevIndex = server.nextIndex[peer] - 1;
- var lastIndex = Math.min(prevIndex + BATCH_SIZE,
- server.log.length);
- if (server.matchIndex[peer] + 1 < server.nextIndex[peer])
- lastIndex = prevIndex;
- sendRequest(model, {
- from: server.id,
- to: peer,
- type: 'AppendEntries',
- term: server.term,
- prevIndex: prevIndex,
- prevTerm: logTerm(server.log, prevIndex),
- entries: server.log.slice(prevIndex, lastIndex),
- commitIndex: Math.min(server.commitIndex, lastIndex)});
- server.rpcDue[peer] = model.time + RPC_TIMEOUT;
- server.heartbeatDue[peer] = model.time + ELECTION_TIMEOUT / 2;
- }
-};
-
-rules.advanceCommitIndex = function(model, server) {
- var matchIndexes = util.mapValues(server.matchIndex).concat(server.log.length);
- matchIndexes.sort(util.numericCompare);
- var n = matchIndexes[Math.floor(NUM_SERVERS / 2)];
- if (server.state == 'leader' &&
- logTerm(server.log, n) == server.term) {
- server.commitIndex = Math.max(server.commitIndex, n);
- }
-};
-
-var handleRequestVoteRequest = function(model, server, request) {
- if (server.term < request.term)
- stepDown(model, server, request.term);
- var 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;
- server.votedFor = request.from;
- server.electionAlarm = makeElectionAlarm(model.time);
- }
- sendReply(model, request, {
- term: server.term,
- granted: granted,
- });
-};
-
-var handleRequestVoteReply = function(model, server, reply) {
- if (server.term < reply.term)
- stepDown(model, server, reply.term);
- if (server.state == 'candidate' &&
- server.term == reply.term) {
- server.rpcDue[reply.from] = util.Inf;
- server.voteGranted[reply.from] = reply.granted;
- }
-};
-
-var handleAppendEntriesRequest = function(model, server, request) {
- var success = false;
- var matchIndex = 0;
- if (server.term < request.term)
- stepDown(model, server, request.term);
- if (server.term == request.term) {
- server.state = 'follower';
- server.electionAlarm = makeElectionAlarm(model.time);
- if (request.prevIndex === 0 ||
- (request.prevIndex <= server.log.length &&
- logTerm(server.log, request.prevIndex) == request.prevTerm)) {
- success = true;
- var index = request.prevIndex;
- for (var i = 0; i < request.entries.length; i += 1) {
- index += 1;
- if (logTerm(server.log, index) != request.entries[i].term) {
- while (server.log.length > index - 1)
- server.log.pop();
- server.log.push(request.entries[i]);
- }
- }
- matchIndex = index;
- server.commitIndex = Math.max(server.commitIndex,
- request.commitIndex);
- }
- }
- sendReply(model, request, {
- term: server.term,
- success: success,
- matchIndex: matchIndex,
- });
-};
-
-var handleAppendEntriesReply = function(model, server, reply) {
- if (server.term < reply.term)
- stepDown(model, server, reply.term);
- if (server.state == 'leader' &&
- server.term == reply.term) {
- if (reply.success) {
- server.matchIndex[reply.from] = Math.max(server.matchIndex[reply.from],
- reply.matchIndex);
- server.nextIndex[reply.from] = reply.matchIndex + 1;
- } else {
- server.nextIndex[reply.from] = Math.max(1, server.nextIndex[reply.from] - 1);
- }
- server.rpcDue[reply.from] = 0;
- }
-};
-
-var handleMessage = function(model, server, message) {
- if (server.state == 'stopped')
- return;
- if (message.type == 'RequestVote') {
- if (message.direction == 'request')
- handleRequestVoteRequest(model, server, message);
- else
- handleRequestVoteReply(model, server, message);
- } else if (message.type == 'AppendEntries') {
- if (message.direction == 'request')
- handleAppendEntriesRequest(model, server, message);
- else
- handleAppendEntriesReply(model, server, message);
- }
-};
-
-
-raft.update = function(model) {
- model.servers.forEach(function(server) {
- rules.startNewElection(model, server);
- rules.becomeLeader(model, server);
- rules.advanceCommitIndex(model, server);
- server.peers.forEach(function(peer) {
- rules.sendRequestVote(model, server, peer);
- rules.sendAppendEntries(model, server, peer);
- });
- });
- var deliver = [];
- var keep = [];
- model.messages.forEach(function(message) {
- if (message.recvTime <= model.time)
- deliver.push(message);
- else if (message.recvTime < util.Inf)
- keep.push(message);
- });
- model.messages = keep;
- deliver.forEach(function(message) {
- model.servers.forEach(function(server) {
- if (server.id == message.to) {
- handleMessage(model, server, message);
- }
- });
- });
-};
-
-raft.stop = function(model, server) {
- server.state = 'stopped';
- server.electionAlarm = 0;
-};
-
-raft.resume = function(model, server) {
- server.state = 'follower';
- server.electionAlarm = makeElectionAlarm(model.time);
-};
-
-raft.resumeAll = function(model) {
- model.servers.forEach(function(server) {
- raft.resume(model, server);
- });
-};
-
-raft.restart = function(model, server) {
- raft.stop(model, server);
- raft.resume(model, server);
-};
-
-raft.drop = function(model, message) {
- model.messages = model.messages.filter(function(m) {
- return m !== message;
- });
-};
-
-raft.timeout = function(model, server) {
- server.state = 'follower';
- server.electionAlarm = 0;
- rules.startNewElection(model, server);
-};
-
-raft.clientRequest = function(model, server) {
- if (server.state == 'leader') {
- server.log.push({term: server.term,
- value: 'v'});
- }
-};
-
-raft.spreadTimers = function(model) {
- var timers = [];
- model.servers.forEach(function(server) {
- if (server.electionAlarm > model.time &&
- server.electionAlarm < util.Inf) {
- timers.push(server.electionAlarm);
- }
- });
- timers.sort(util.numericCompare);
- if (timers.length > 1 &&
- timers[1] - timers[0] < MAX_RPC_LATENCY) {
- if (timers[0] > model.time + MAX_RPC_LATENCY) {
- model.servers.forEach(function(server) {
- if (server.electionAlarm == timers[0]) {
- server.electionAlarm -= MAX_RPC_LATENCY;
- console.log('adjusted S' + server.id + ' timeout forward');
- }
- });
- } else {
- model.servers.forEach(function(server) {
- if (server.electionAlarm > timers[0] &&
- server.electionAlarm < timers[0] + MAX_RPC_LATENCY) {
- server.electionAlarm += MAX_RPC_LATENCY;
- console.log('adjusted S' + server.id + ' timeout backward');
- }
- });
- }
- }
-};
-
-raft.alignTimers = function(model) {
- raft.spreadTimers(model);
- var timers = [];
- model.servers.forEach(function(server) {
- if (server.electionAlarm > model.time &&
- server.electionAlarm < util.Inf) {
- timers.push(server.electionAlarm);
- }
- });
- timers.sort(util.numericCompare);
- model.servers.forEach(function(server) {
- if (server.electionAlarm == timers[1]) {
- server.electionAlarm = timers[0];
- console.log('adjusted S' + server.id + ' timeout forward');
- }
- });
-};
-
-raft.setupLogReplicationScenario = function(model) {
- var 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]);
- rules.startNewElection(model, s1);
- model.servers[1].term = 2;
- model.servers[2].term = 2;
- model.servers[3].term = 2;
- model.servers[4].term = 2;
- model.servers[1].votedFor = 1;
- model.servers[2].votedFor = 1;
- 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]);
- rules.becomeLeader(model, s1);
- raft.clientRequest(model, s1);
- raft.clientRequest(model, s1);
- raft.clientRequest(model, s1);
-};
-
-})();
diff --git a/script.js b/script.js
index 6617ef7..2d7bac2 100644
--- a/script.js
+++ b/script.js
@@ -3,12 +3,22 @@
/* jshint devel: true */
/* jshint jquery: true */
/* global util */
-/* global raft */
+/* global pala */
/* global makeState */
/* global ELECTION_TIMEOUT */
/* global NUM_SERVERS */
+/* global SERVER_STATES */
+/* global REQUEST_TYPES */
+/* global MESSAGE_DIRECTIONS */
+
'use strict';
+const START_PROPOSER_IDX = 1
+
+const TERM_COLORS = {
+ recovery: 6,
+}
+
var playback;
var render = {};
var state;
@@ -43,6 +53,7 @@ var termColors = [
'#e78ac3',
'#a6d854',
'#ffd92f',
+ '#fe4c4c'
];
var SVG = function(tag) {
@@ -88,10 +99,11 @@ playback = function() {
for (var i = 1; i <= NUM_SERVERS; i += 1) {
var peers = [];
for (var j = 1; j <= NUM_SERVERS; j += 1) {
- if (i != j)
+ if (i !== j)
peers.push(j);
}
- state.current.servers.push(raft.server(i, peers));
+ const isProposer = i === START_PROPOSER_IDX
+ state.current.servers.push(pala.server(i, peers, isProposer, isProposer ? START_PROPOSER_IDX : undefined));
}
})();
@@ -213,17 +225,27 @@ render.clock = function() {
};
var serverActions = [
- ['stop', raft.stop],
- ['resume', raft.resume],
- ['restart', raft.restart],
- ['time out', raft.timeout],
- ['request', raft.clientRequest],
+ ['stop', pala.stop],
+ ['resume', pala.resume],
+ ['restart', pala.restart],
+ ['time out', pala.timeout],
+ ['request', pala.clientRequest],
];
var messageActions = [
- ['drop', raft.drop],
+ ['drop', pala.drop],
];
+const chooseNodeColor = (server) => {
+ if(server.state === SERVER_STATES.stopped) {
+ return 'gray'
+ }
+ if(server.state === SERVER_STATES.recovery) {
+ return termColors[TERM_COLORS.recovery]
+ }
+ return termColors[server.epoch % termColors.length]
+}
+
render.servers = function(serversSame) {
state.current.servers.forEach(function(server) {
var serverNode = $('#server-' + server.id, svg);
@@ -233,29 +255,27 @@ render.servers = function(serversSame) {
(ELECTION_TIMEOUT * 2),
0, 1)));
if (!serversSame) {
- $('text.term', serverNode).text(server.term);
+ $('text.term', serverNode).text(server.epoch);
serverNode.attr('class', 'server ' + server.state);
$('circle.background', serverNode)
- .attr('style', 'fill: ' +
- (server.state == 'stopped' ? 'gray'
- : termColors[server.term % termColors.length]));
+ .attr('style', 'fill: ' + chooseNodeColor(server));
var votesGroup = $('.votes', serverNode);
votesGroup.empty();
- if (server.state == 'candidate') {
+ if (server.state === SERVER_STATES.recovery) {
state.current.servers.forEach(function (peer) {
var coord = util.circleCoord((peer.id - 1) / NUM_SERVERS,
serverSpec(server.id).cx,
serverSpec(server.id).cy,
serverSpec(server.id).r * 5/8);
var state;
- if (peer == server || server.voteGranted[peer.id]) {
+ if (peer === server || server.voteGranted[peer.id]) {
state = 'have';
- } else if (peer.votedFor == server.id && peer.term == server.term) {
+ } else if (peer.votedFor === server.id && peer.epoch === server.epoch) {
state = 'coming';
} else {
state = 'no';
}
- var granted = (peer == server ? true : server.voteGranted[peer.id]);
+ var granted = (peer === server ? true : server.voteGranted[peer.id]);
votesGroup.append(
SVG('circle')
.attr({
@@ -306,11 +326,11 @@ render.entry = function(spec, entry, committed) {
.append(SVG('rect')
.attr(spec)
.attr('stroke-dasharray', committed ? '1 0' : '5 5')
- .attr('style', 'fill: ' + termColors[entry.term % termColors.length]))
+ .attr('style', 'fill: ' + termColors[entry.epoch % termColors.length]))
.append(SVG('text')
.attr({x: spec.x + spec.width / 2,
y: spec.y + spec.height / 2})
- .text(entry.term));
+ .text(entry.epoch));
};
render.logs = function() {
@@ -381,7 +401,7 @@ render.logs = function() {
entry,
index <= server.commitIndex));
});
- if (leader !== null && leader != server) {
+ if (leader !== null && leader !== server) {
log.append(
SVG('circle')
.attr('title', 'match index')//.tooltip({container: 'body'})
@@ -410,7 +430,7 @@ render.messages = function(messagesSame) {
.attr('title', message.type + ' ' + message.direction)//.tooltip({container: 'body'})
.append(SVG('circle'))
.append(SVG('path').attr('class', 'message-direction'));
- if (message.direction == 'reply')
+ if (message.direction === MESSAGE_DIRECTIONS.reply)
a.append(SVG('path').attr('class', 'message-success'));
messagesGroup.append(a);
});
@@ -448,24 +468,24 @@ render.messages = function(messagesSame) {
});
}
state.current.messages.forEach(function(message, i) {
- var s = messageSpec(message.from, message.to,
+ const s = messageSpec(message.from, message.to,
(state.current.time - message.sendTime) /
(message.recvTime - message.sendTime));
$('#message-' + i + ' circle', messagesGroup)
.attr(s);
- if (message.direction == 'reply') {
- var dlist = [];
+ if (message.direction === MESSAGE_DIRECTIONS.reply) {
+ const dlist = [];
dlist.push('M', s.cx - s.r, comma, s.cy,
'L', s.cx + s.r, comma, s.cy);
- if ((message.type == 'RequestVote' && message.granted) ||
- (message.type == 'AppendEntries' && message.success)) {
+ if ((message.type === REQUEST_TYPES.requestVote && message.granted) ||
+ (message.type === REQUEST_TYPES.appendEntries && message.success)) {
dlist.push('M', s.cx, comma, s.cy - s.r,
'L', s.cx, comma, s.cy + s.r);
}
$('#message-' + i + ' path.message-success', messagesGroup)
.attr('d', dlist.join(' '));
}
- var dir = $('#message-' + i + ' path.message-direction', messagesGroup);
+ const dir = $('#message-' + i + ' path.message-direction', messagesGroup);
if (playback.isPaused()) {
dir.attr('style', 'marker-end:url(#TriangleOutS-' + message.type + ')')
.attr('d',
@@ -479,7 +499,7 @@ render.messages = function(messagesSame) {
};
var relTime = function(time, now) {
- if (time == util.Inf)
+ if (time === util.Inf)
return 'infinity';
var sign = time > now ? '+' : '';
return sign + ((time - now) / 1e3).toFixed(3) + 'ms';
@@ -521,7 +541,7 @@ serverModal = function(model, server) {
.empty()
.append($(' ')
.append(li('state', server.state))
- .append(li('currentTerm', server.term))
+ .append(li('currentEpoch', server.epoch))
.append(li('votedFor', server.votedFor))
.append(li('commitIndex', server.commitIndex))
.append(li('electionAlarm', relTime(server.electionAlarm, model.time)))
@@ -555,18 +575,18 @@ messageModal = function(model, message) {
.append(li('to', 'S' + message.to))
.append(li('sent', relTime(message.sendTime, model.time)))
.append(li('deliver', relTime(message.recvTime, model.time)))
- .append(li('term', message.term));
- if (message.type == 'RequestVote') {
- if (message.direction == 'request') {
+ .append(li('epoch', message.epoch));
+ if (message.type === REQUEST_TYPES.requestVote) {
+ if (message.direction === MESSAGE_DIRECTIONS.request) {
fields.append(li('lastLogIndex', message.lastLogIndex));
fields.append(li('lastLogTerm', message.lastLogTerm));
} else {
fields.append(li('granted', message.granted));
}
- } else if (message.type == 'AppendEntries') {
- if (message.direction == 'request') {
+ } else if (message.type === REQUEST_TYPES.appendEntries) {
+ if (message.direction === MESSAGE_DIRECTIONS.request) {
var entries = '[' + message.entries.map(function(e) {
- return e.term;
+ return e.epoch;
}).join(' ') + ']';
fields.append(li('prevIndex', message.prevIndex));
fields.append(li('prevTerm', message.prevTerm));
@@ -612,7 +632,7 @@ render.update = function() {
// value hasn't changed.
var serversSame = false;
var messagesSame = false;
- if (lastRenderedO == state.current) {
+ if (lastRenderedO === state.current) {
serversSame = util.equals(lastRenderedV.servers, state.current.servers);
messagesSame = util.equals(lastRenderedV.messages, state.current.messages);
}
@@ -658,7 +678,7 @@ $(window).keyup(function(e) {
} else if (e.keyCode == 'C'.charCodeAt(0)) {
if (leader !== null) {
state.fork();
- raft.clientRequest(state.current, leader);
+ pala.clientRequest(state.current, leader);
state.save();
render.update();
$('.modal').modal('hide');
@@ -666,34 +686,34 @@ $(window).keyup(function(e) {
} else if (e.keyCode == 'R'.charCodeAt(0)) {
if (leader !== null) {
state.fork();
- raft.stop(state.current, leader);
- raft.resume(state.current, leader);
+ pala.stop(state.current, leader);
+ pala.resume(state.current, leader);
state.save();
render.update();
$('.modal').modal('hide');
}
} else if (e.keyCode == 'T'.charCodeAt(0)) {
state.fork();
- raft.spreadTimers(state.current);
+ pala.spreadTimers(state.current);
state.save();
render.update();
$('.modal').modal('hide');
} else if (e.keyCode == 'A'.charCodeAt(0)) {
state.fork();
- raft.alignTimers(state.current);
+ pala.alignTimers(state.current);
state.save();
render.update();
$('.modal').modal('hide');
} else if (e.keyCode == 'L'.charCodeAt(0)) {
state.fork();
playback.pause();
- raft.setupLogReplicationScenario(state.current);
+ pala.setupLogReplicationScenario(state.current);
state.save();
render.update();
$('.modal').modal('hide');
} else if (e.keyCode == 'B'.charCodeAt(0)) {
state.fork();
- raft.resumeAll(state.current);
+ pala.resumeAll(state.current);
state.save();
render.update();
$('.modal').modal('hide');
@@ -713,12 +733,12 @@ $('#modal-details').on('show.bs.modal', function(e) {
var getLeader = function() {
var leader = null;
- var term = 0;
+ var epoch = 0;
state.current.servers.forEach(function(server) {
- if (server.state == 'leader' &&
- server.term > term) {
+ if (server.state == SERVER_STATES.leader &&
+ server.epoch > epoch) {
leader = server;
- term = server.term;
+ epoch = server.epoch;
}
});
return leader;
@@ -766,7 +786,7 @@ $('#time-button')
// $('[data-toggle="tooltip"]').tooltip();
state.updater = function(state) {
- raft.update(state.current);
+ pala.update(state.current);
var time = state.current.time;
var base = state.base(time);
state.current.time = base.time;
diff --git a/style.css b/style.css
index a4492fe..0019032 100644
--- a/style.css
+++ b/style.css
@@ -75,7 +75,7 @@ svg .server .votes .have {
}
svg .server .votes .coming {
- fill: gray;
+ fill: none;
stroke: black;
}
@@ -98,6 +98,16 @@ svg .message.AppendEntries {
stroke: #fc8d62;
}
+svg .message.RecoveryMessage {
+ fill: #4253fc;
+ stroke: #4253fc;
+}
+
+svg .message.EpochChanging {
+ fill: #ffdd33;
+ stroke: #ffdd33;
+}
+
svg .message.request circle {
}