UNPKG

hamok

Version:

Lightweight Distributed Object Storage on RAFT consensus algorithm

189 lines (188 loc) 10.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.createRaftFollowerState = createRaftFollowerState; const logger_1 = require("../common/logger"); const RaftAppendEntriesRequest_1 = require("./RaftAppendEntriesRequest"); const RaftCandidateState_1 = require("./RaftCandidateState"); const logger = (0, logger_1.createLogger)('RaftFollowerState'); function createRaftFollowerState(context) { const { raftEngine } = context; const config = raftEngine.config; const messageEmitter = raftEngine.transport; const props = raftEngine.props; const localPeerId = raftEngine.localPeerId; const logs = raftEngine.logs; const pendingRequests = new Map(); let updated = Date.now(); let closed = false; let currentTerm = props.currentTerm; const updateCommitIndex = (leaderCommitIndex) => { logger.trace('%s updateCommitIndex leaderCommitIndex: %d, logsCommitIndex: %d', localPeerId, leaderCommitIndex, logs.commitIndex); if (leaderCommitIndex <= logs.commitIndex) { return; } const expectedCommitIndex = Math.min(logs.nextIndex - 1, leaderCommitIndex); if (expectedCommitIndex <= logs.commitIndex) { return; } const committedLogEntries = logs.commitUntil(expectedCommitIndex); logger.trace('%s updateCommitIndex committedLogEntries: %d', localPeerId, committedLogEntries.length); for (const logEntry of committedLogEntries) { logger.trace('%s commit log entry %d', localPeerId, logEntry.index); raftEngine.events.emit('commit', logEntry.index, logEntry.entry); } }; const appendEntriesRequestListener = (requestChunk) => { logger.trace('%s received RaftAppendEntriesRequestChunk %o', localPeerId, requestChunk); if (requestChunk.term < currentTerm) { logger.warn(`Append entries request appeared from a previous term. currentTerm: ${currentTerm}, received entries request term: ${requestChunk.term}`); const response = requestChunk.createResponse(false, -1, false); return messageEmitter.send(response); } if (currentTerm < requestChunk.term) { currentTerm = requestChunk.term; props.currentTerm = currentTerm; props.votedFor = undefined; logger.debug(`Term for follower has been changed from ${currentTerm} to ${requestChunk.term}`); } // let's restart the timer updated = Date.now(); // set the actual leader if (requestChunk.leaderId !== undefined) { raftEngine.leaderId = requestChunk.leaderId; context.extraWaitingTime = undefined; } if (requestChunk.entry === undefined && requestChunk.sequence === 0) { if (requestChunk.lastMessage === false) { logger.warn('Entries cannot be null if it is a part of chunks and thats not the last message'); const response = requestChunk.createResponse(false, -1, true); return messageEmitter.send(response); } // that was a keep alive message updateCommitIndex(requestChunk.leaderCommit); const response = requestChunk.createResponse(true, logs.nextIndex, true); return messageEmitter.send(response); } // assemble here let request = pendingRequests.get(requestChunk.requestId); if (request === undefined) { request = new RaftAppendEntriesRequest_1.RaftAppendEntriesRequest(requestChunk.requestId); pendingRequests.set(requestChunk.requestId, request); } request.add(requestChunk); if (request.ready === false) { const response = requestChunk.createResponse(true, -1, false); return messageEmitter.send(response); } pendingRequests.delete(requestChunk.requestId); logger.trace('%s Received RaftAppendEntriesRequest %o Entries: %d', localPeerId, request, request.entries?.length); if (logs.nextIndex < request.leaderNextIndex - request.entries.length) { const newCommitIndex = Math.max(0, request.leaderNextIndex - (request.entries?.length ?? 0)); logger.warn('%s Resetting commit index to %d. The current next index for logs is %d, and the leader index is %d. ' + 'the leader has %d number of logs to send, which is insufficient to close the gap between this peer and the leader.' + ' If snapshots for storages do not close the gap that can lead to inconsistency!', localPeerId, newCommitIndex, logs.nextIndex, request.leaderNextIndex, request.entries?.length); raftEngine.logs.reset(newCommitIndex); // logger.warn('WTF %o', [ ...request.entries ].join(', ')); } // if we arrived in this point we know that the sync is possible. const entryLength = request.entries.length; const localNextIndex = logs.nextIndex; let success = true; for (let i = 0; i < entryLength; ++i) { const logIndex = request.leaderNextIndex - entryLength + i; const entry = request.entries[i]; if (logIndex < localNextIndex) { const oldLogEntry = logs.compareAndOverride(logIndex, currentTerm, entry); if (oldLogEntry != null && currentTerm < oldLogEntry.term) { logger.warn(`We overrode an entry coming from a higher term we currently had. (currentTerm: ${currentTerm}, old log entry term: ${oldLogEntry.term}). This can cause a potential inconsistency if other peer has not override it as well`); } } else if (!logs.compareAndAdd(logIndex, currentTerm, entry)) { logger.warn('Log for index {} not added, though it supposed to', logIndex); success = false; // } else { // logger.info('%s Log for index %d added', localPeerId, logIndex); } } updateCommitIndex(requestChunk.leaderCommit); const response = requestChunk.createResponse(success, logs.nextIndex, true); messageEmitter.send(response); }; const voteRequestListener = (request) => { logger.debug('%s Received a vote request %o, votedFor: %s', localPeerId, request, props.votedFor); // if (raftEngine.leaderId !== undefined) { // if we know the leader, we should not vote for anyone else, until the leader is alive // return messageEmitter.send(request.createResponse(false)); // } if (request.term <= props.currentTerm) { // someone requested a vote from a previous or equal term. logger.warn('The candidate %s requested a vote from a previous or equal term. The current term is %d.', request.candidateId, props.currentTerm); return messageEmitter.send(request.createResponse(false)); } if (0 < logs.commitIndex) { // if this follower has committed logs! if (request.lastLogIndex < logs.commitIndex) { // if the highest index of the candidate is smaller than the commit index of this, // then that candidate should not lead this cluster, and wait for another leader who can logger.warn('The candidate %s has a smaller last log index than the commit index of this follower %s. The candidate should not lead this cluster.', request.candidateId, localPeerId); return messageEmitter.send(request.createResponse(false)); } } let voteGranted = props.votedFor === undefined; if (voteGranted) { // when we vote for this candidate for the first time, // let's restart the timer, so we wait a bit more to give time to win the election before we run for presidency. props.votedFor = request.candidateId; updated = Date.now(); } else { // maybe we already voted for the candidate itself? voteGranted = props.votedFor === request.candidateId; } const response = request.createResponse(voteGranted); logger.debug(`${localPeerId} send a vote response %o.`, response); messageEmitter.send(response); }; const close = () => { if (closed) return; closed = true; messageEmitter.off('RaftVoteRequest', voteRequestListener); messageEmitter.off('RaftAppendEntriesRequestChunk', appendEntriesRequestListener); logger.debug('%s FollowerState is closed', localPeerId); }; messageEmitter.on('RaftVoteRequest', voteRequestListener); messageEmitter.on('RaftAppendEntriesRequestChunk', appendEntriesRequestListener); const run = () => { const now = Date.now(); const elapsedInMs = now - updated; if (elapsedInMs <= config.followerMaxIdleInMs + (context.extraWaitingTime ?? 0)) { // we have still time before we start an election return; } // we don't know a leader at this point raftEngine.leaderId = undefined; raftEngine.props.votedFor = undefined; if (raftEngine.remotePeers.size < 1) { // if we are alone, there is no point to start an election // so we just restart the timer return (updated = now); } logger.debug(`${localPeerId} is timed out to wait for append logs request (maxIdle: ${config.followerMaxIdleInMs}, elapsed: ${elapsedInMs}) extraWaitingTime: ${context.extraWaitingTime}`); if (config.onlyFollower) { updated = now; context.extraWaitingTime = Math.max(config.followerMaxIdleInMs, Math.min((context.extraWaitingTime ?? 0) * 2, 30000)); return logger.debug('This peer is configured to be only a follower. It will not start an election.'); } raftEngine.state = (0, RaftCandidateState_1.createRaftCandidateState)({ electionTerm: props.currentTerm + 1, raftEngine, }); }; return { stateName: 'follower', run, close, }; }