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 { }