net.kuujo.copycat.raft.FollowerState Maven / Gradle / Ivy
The newest version!
/*
* Copyright 2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.kuujo.copycat.raft;
import net.kuujo.copycat.raft.protocol.*;
import net.kuujo.copycat.util.internal.Quorum;
import java.nio.ByteBuffer;
import java.util.Random;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Follower state.
*
* @author Jordan Halterman
*/
class FollowerState extends ActiveState {
private final Random random = new Random();
private ScheduledFuture> currentTimer;
public FollowerState(RaftContext context) {
super(context);
}
@Override
public Type type() {
return Type.FOLLOWER;
}
@Override
public synchronized CompletableFuture open() {
return super.open().thenRun(this::startHeartbeatTimeout);
}
/**
* Starts the heartbeat timer.
*/
private void startHeartbeatTimeout() {
LOGGER.debug("{} - Starting heartbeat timer", context.getLocalMember());
resetHeartbeatTimeout();
}
/**
* Resets the heartbeat timer.
*/
private void resetHeartbeatTimeout() {
context.checkThread();
if (isClosed()) return;
// If a timer is already set, cancel the timer.
if (currentTimer != null) {
LOGGER.debug("{} - Reset heartbeat timeout", context.getLocalMember());
currentTimer.cancel(false);
}
// Set the election timeout in a semi-random fashion with the random range
// being election timeout and 2 * election timeout.
long delay = context.getElectionTimeout() + (random.nextInt((int) context.getElectionTimeout()) % context.getElectionTimeout());
currentTimer = context.executor().schedule(() -> {
currentTimer = null;
if (isOpen()) {
if (context.getLastVotedFor() == null) {
LOGGER.info("{} - Heartbeat timed out in {} milliseconds", context.getLocalMember(), delay);
sendPollRequests();
} else {
// If the node voted for a candidate then reset the election timer.
resetHeartbeatTimeout();
}
}
}, delay, TimeUnit.MILLISECONDS);
}
/**
* Polls all members of the cluster to determine whether this member should transition to the CANDIDATE state.
*/
private void sendPollRequests() {
// Set a new timer within which other nodes must respond in order for this node to transition to candidate.
currentTimer = context.executor().schedule(() -> {
LOGGER.debug("{} - Failed to poll a majority of the cluster in {} milliseconds", context.getLocalMember(), context.getElectionTimeout());
resetHeartbeatTimeout();
}, context.getElectionTimeout(), TimeUnit.MILLISECONDS);
// Create a quorum that will track the number of nodes that have responded to the poll request.
final AtomicBoolean complete = new AtomicBoolean();
final Quorum quorum = new Quorum(1 + (int) Math.floor(context.getActiveMembers().size() / 2.0), (elected) -> {
// If a majority of the cluster indicated they would vote for us then transition to candidate.
complete.set(true);
if (elected) {
transition(Type.CANDIDATE);
} else {
resetHeartbeatTimeout();
}
});
// First, load the last log entry to get its term. We load the entry
// by its index since the index is required by the protocol.
Long lastIndex = context.log().lastIndex();
ByteBuffer lastEntry = lastIndex != null ? context.log().getEntry(lastIndex) : null;
// Once we got the last log term, iterate through each current member
// of the cluster and vote each member for a vote.
LOGGER.info("{} - Polling members {}", context.getLocalMember(), context.getActiveMembers());
final Long lastTerm = lastEntry != null ? lastEntry.getLong() : null;
for (String member : context.getActiveMembers()) {
LOGGER.debug("{} - Polling {} for next term {}", context.getLocalMember(), member, context.getTerm() + 1);
PollRequest request = PollRequest.builder()
.withUri(member)
.withTerm(context.getTerm())
.withCandidate(context.getLocalMember())
.withLogIndex(lastIndex)
.withLogTerm(lastTerm)
.build();
pollHandler.apply(request).whenCompleteAsync((response, error) -> {
context.checkThread();
if (isOpen() && !complete.get()) {
if (error != null) {
LOGGER.debug("{} - Failed to poll {}. Reason: {}", context.getLocalMember(), member, error.getMessage());
quorum.fail();
} else {
if (response.term() > context.getTerm()) {
context.setTerm(response.term());
}
if (!response.accepted()) {
LOGGER.info("{} - Received rejected poll from {}", context.getLocalMember(), member);
quorum.fail();
} else if (response.term() != context.getTerm()) {
LOGGER.info("{} - Received accepted poll for a different term from {}", context.getLocalMember(), member);
quorum.fail();
} else {
LOGGER.info("{} - Received accepted poll from {}", context.getLocalMember(), member);
quorum.succeed();
}
}
}
}, context.executor());
}
}
@Override
public CompletableFuture append(AppendRequest request) {
resetHeartbeatTimeout();
CompletableFuture response = super.append(request);
resetHeartbeatTimeout();
return response;
}
@Override
protected VoteResponse handleVote(VoteRequest request) {
// Reset the heartbeat timeout if we voted for another candidate.
VoteResponse response = super.handleVote(request);
if (response.voted()) {
resetHeartbeatTimeout();
}
return response;
}
/**
* Cancels the heartbeat timeout.
*/
private void cancelHeartbeatTimeout() {
if (currentTimer != null) {
LOGGER.debug("{} - Cancelling heartbeat timer", context.getLocalMember());
currentTimer.cancel(false);
}
}
@Override
public synchronized CompletableFuture close() {
return super.close().thenRun(this::cancelHeartbeatTimeout);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy