hamok
Version:
Lightweight Distributed Object Storage on RAFT consensus algorithm
189 lines (188 loc) • 10.2 kB
JavaScript
;
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,
};
}