/** * Copyright 2013 David Rusek <dave dot rusek at gmail dot com> * * 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 org.robotninjas.barge.state; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Optional; import com.google.common.collect.Lists; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import org.jetlang.fibers.Fiber; import org.robotninjas.barge.RaftExecutor; import org.robotninjas.barge.Replica; import org.robotninjas.barge.api.RequestVote; import org.robotninjas.barge.api.RequestVoteResponse; import org.robotninjas.barge.log.RaftLog; import org.robotninjas.barge.rpc.Client; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.annotation.concurrent.NotThreadSafe; import javax.inject.Inject; import java.util.List; import java.util.Random; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.util.concurrent.Futures.addCallback; import static org.robotninjas.barge.state.MajorityCollector.majorityResponse; import static org.robotninjas.barge.state.Raft.StateType.*; import static org.robotninjas.barge.state.RaftPredicates.voteGranted; @NotThreadSafe class Candidate extends BaseState { private static final Logger LOGGER = LoggerFactory.getLogger(Candidate.class); private static final Random RAND = new Random(System.nanoTime()); private final Fiber scheduler; private final long electionTimeout; private final Client client; private DeadlineTimer electionTimer; private ListenableFuture<Boolean> electionResult; @Inject Candidate(RaftLog log, @RaftExecutor Fiber scheduler, @ElectionTimeout long electionTimeout, Client client) { super(CANDIDATE, log); this.scheduler = scheduler; this.electionTimeout = electionTimeout; this.client = client; } @Override public void init(@Nonnull final RaftStateContext ctx) { final RaftLog log = getLog(); log.currentTerm(log.currentTerm() + 1); log.votedFor(Optional.of(log.self())); LOGGER.debug("Election starting for term {}", log.currentTerm()); List<ListenableFuture<RequestVoteResponse>> responses = Lists.newArrayList(); // Request votes from peers for (Replica replica : log.members()) { responses.add(sendVoteRequest(ctx, replica)); } // We always vote for ourselves responses.add(Futures.immediateFuture(RequestVoteResponse.newBuilder().setVoteGranted(true).build())); electionResult = majorityResponse(responses, voteGranted()); long timeout = electionTimeout + (RAND.nextLong() % electionTimeout); electionTimer = DeadlineTimer.start(scheduler, new Runnable() { @Override public void run() { LOGGER.debug("Election timeout"); ctx.setState(Candidate.this, CANDIDATE); } }, timeout); addCallback(electionResult, new FutureCallback<Boolean>() { @Override public void onSuccess(@Nullable Boolean elected) { checkNotNull(elected); //noinspection ConstantConditions if (elected) { ctx.setState(Candidate.this, LEADER); } } @Override public void onFailure(Throwable t) { if (!electionResult.isCancelled()) { LOGGER.debug("Election failed with exception:", t); } } }); } @Override public void destroy(RaftStateContext ctx) { electionResult.cancel(false); electionTimer.cancel(); } @VisibleForTesting ListenableFuture<RequestVoteResponse> sendVoteRequest(RaftStateContext ctx, Replica replica) { RaftLog log = getLog(); RequestVote request = RequestVote.newBuilder() .setTerm(log.currentTerm()) .setCandidateId(log.self().toString()) .setLastLogIndex(log.lastLogIndex()) .setLastLogTerm(log.lastLogTerm()) .build(); ListenableFuture<RequestVoteResponse> response = client.requestVote(replica, request); Futures.addCallback(response, checkTerm(ctx)); return response; } private FutureCallback<RequestVoteResponse> checkTerm(final RaftStateContext ctx) { return new FutureCallback<RequestVoteResponse>() { @Override public void onSuccess(@Nullable RequestVoteResponse response) { if (response.getTerm() > getLog().currentTerm()) { getLog().currentTerm(response.getTerm()); ctx.setState(Candidate.this, FOLLOWER); } } @Override public void onFailure(Throwable t) { } }; } }