package org.robotninjas.barge.state;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import org.robotninjas.barge.NoLeaderException;
import org.robotninjas.barge.NotLeaderException;
import org.robotninjas.barge.RaftException;
import org.robotninjas.barge.Replica;
import org.robotninjas.barge.api.AppendEntries;
import org.robotninjas.barge.api.AppendEntriesResponse;
import org.robotninjas.barge.api.RequestVote;
import org.robotninjas.barge.api.RequestVoteResponse;
import org.robotninjas.barge.log.RaftLog;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import static com.google.common.base.Preconditions.checkNotNull;
import static org.robotninjas.barge.state.Raft.StateType.*;
import static org.robotninjas.barge.state.RaftStateContext.StateType;
public abstract class BaseState implements State {
private final StateType type;
private final RaftLog log;
private Optional<Replica> leader;
protected BaseState(@Nullable StateType type, @Nonnull RaftLog log) {
this.log = checkNotNull(log);
this.type = type;
}
@Nullable
@Override
public StateType type() {
return type;
}
protected RaftLog getLog() {
return log;
}
@Override
public void destroy(RaftStateContext ctx) {
}
@VisibleForTesting
boolean shouldVoteFor(@Nonnull RaftLog log, @Nonnull RequestVote request) {
// If votedFor is null or candidateId, and candidate's log is at
// least as up-to-date as receiver’s log, grant vote (§5.2, §5.4)
Optional<Replica> votedFor = log.votedFor();
Replica candidate = log.getReplica(request.getCandidateId());
if (votedFor.isPresent()) {
if (!votedFor.get().equals(candidate)) {
return false;
}
}
assert !votedFor.isPresent() || votedFor.get().equals(candidate);
boolean logIsComplete;
if (request.getLastLogTerm() > log.lastLogTerm()) {
logIsComplete = true;
} else if (request.getLastLogTerm() == log.lastLogTerm()) {
if (request.getLastLogIndex() >= log.lastLogIndex()) {
logIsComplete = true;
} else {
logIsComplete = false;
}
} else {
logIsComplete = false;
}
if (logIsComplete) {
// Requestor has an up-to-date log, we haven't voted for anyone else => OK
return true;
}
return false;
}
protected void resetTimer() {
}
@Nonnull
@Override
public AppendEntriesResponse appendEntries(@Nonnull RaftStateContext ctx, @Nonnull AppendEntries request) {
boolean success = false;
if (request.getTerm() >= log.currentTerm()) {
if (request.getTerm() > log.currentTerm()) {
log.currentTerm(request.getTerm());
if (ctx.type().equals(LEADER) || ctx.type().equals(CANDIDATE)) {
ctx.setState(this, FOLLOWER);
}
}
leader = Optional.of(log.getReplica(request.getLeaderId()));
resetTimer();
success = log.append(request);
if (request.getCommitIndex() > log.commitIndex()) {
log.commitIndex(Math.min(request.getCommitIndex(), log.lastLogIndex()));
}
}
return AppendEntriesResponse.newBuilder()
.setTerm(log.currentTerm())
.setSuccess(success)
.setLastLogIndex(log.lastLogIndex())
.build();
}
@Nonnull
@Override
public RequestVoteResponse requestVote(@Nonnull RaftStateContext ctx, @Nonnull RequestVote request) {
boolean voteGranted;
long term = request.getTerm();
long currentTerm = log.currentTerm();
if (term < currentTerm) {
// Reply false if term < currentTerm (§5.1)
voteGranted = false;
} else {
// If RPC request or response contains term T > currentTerm:
// set currentTerm = T, convert to follower (§5.1)
if (term > currentTerm) {
log.currentTerm(term);
if (ctx.type().equals(LEADER) || ctx.type().equals(CANDIDATE)) {
ctx.setState(this, FOLLOWER);
}
}
Replica candidate = log.getReplica(request.getCandidateId());
voteGranted = shouldVoteFor(log, request);
if (voteGranted) {
log.votedFor(Optional.of(candidate));
}
}
return RequestVoteResponse.newBuilder()
.setTerm(currentTerm)
.setVoteGranted(voteGranted)
.build();
}
@Nonnull
@Override
public ListenableFuture<Object> commitOperation(@Nonnull RaftStateContext ctx, @Nonnull byte[] operation) throws RaftException {
StateType stateType = ctx.type();
Preconditions.checkNotNull(stateType);
if (stateType.equals(FOLLOWER)) {
throw new NotLeaderException(leader.get());
} else if (stateType.equals(CANDIDATE)) {
throw new NoLeaderException();
}
return Futures.immediateCancelledFuture();
}
@Override
public void doStop(RaftStateContext ctx) {
ctx.setState(this, STOPPED);
}
}