Java tutorial
/** * 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.Maps; import com.google.common.primitives.Longs; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import org.robotninjas.barge.RaftException; import org.robotninjas.barge.Replica; import org.robotninjas.barge.log.RaftLog; import org.robotninjas.barge.rpc.RaftScheduler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nonnegative; import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.annotation.concurrent.NotThreadSafe; import javax.inject.Inject; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.collect.Lists.newArrayList; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static org.robotninjas.barge.proto.RaftProto.*; import static org.robotninjas.barge.state.RaftStateContext.StateType.FOLLOWER; @NotThreadSafe class Leader extends BaseState { private static final Logger LOGGER = LoggerFactory.getLogger(Leader.class); private final RaftLog log; private final ScheduledExecutorService scheduler; private final long timeout; private final Map<Replica, ReplicaManager> managers = Maps.newHashMap(); private final ReplicaManagerFactory replicaManagerFactory; private ScheduledFuture<?> heartbeatTask; @Inject Leader(RaftLog log, @RaftScheduler ScheduledExecutorService scheduler, @ElectionTimeout @Nonnegative long timeout, ReplicaManagerFactory replicaManagerFactory) { this.log = checkNotNull(log); this.scheduler = checkNotNull(scheduler); checkArgument(timeout > 0); this.timeout = timeout; this.replicaManagerFactory = checkNotNull(replicaManagerFactory); } @Override public void init(@Nonnull RaftStateContext ctx) { for (Replica replica : log.members()) { managers.put(replica, replicaManagerFactory.create(replica)); } sendRequests(ctx); resetTimeout(ctx); } private void stepDown(RaftStateContext ctx) { heartbeatTask.cancel(false); for (ReplicaManager mgr : managers.values()) { mgr.shutdown(); } ctx.setState(FOLLOWER); } @Nonnull @Override public RequestVoteResponse requestVote(@Nonnull RaftStateContext ctx, @Nonnull RequestVote request) { LOGGER.debug("RequestVote received for term {}", request.getTerm()); boolean voteGranted = false; if (request.getTerm() > log.currentTerm()) { log.currentTerm(request.getTerm()); stepDown(ctx); Replica candidate = Replica.fromString(request.getCandidateId()); voteGranted = shouldVoteFor(log, request); if (voteGranted) { log.lastVotedFor(Optional.of(candidate)); } } return RequestVoteResponse.newBuilder().setTerm(log.currentTerm()).setVoteGranted(voteGranted).build(); } @Nonnull @Override public AppendEntriesResponse appendEntries(@Nonnull RaftStateContext ctx, @Nonnull AppendEntries request) { boolean success = false; if (request.getTerm() > log.currentTerm()) { log.currentTerm(request.getTerm()); stepDown(ctx); success = log.append(request); } return AppendEntriesResponse.newBuilder().setTerm(log.currentTerm()).setSuccess(success).build(); } void resetTimeout(@Nonnull final RaftStateContext ctx) { if (null != heartbeatTask) { heartbeatTask.cancel(false); } heartbeatTask = scheduler.scheduleAtFixedRate(new Runnable() { @Override public void run() { LOGGER.debug("Sending heartbeat"); sendRequests(ctx); } }, timeout, timeout, MILLISECONDS); } @Nonnull @Override public ListenableFuture<Object> commitOperation(@Nonnull RaftStateContext ctx, @Nonnull byte[] operation) throws RaftException { resetTimeout(ctx); ListenableFuture<Object> result = log.append(operation); sendRequests(ctx); return result; } /** * Find the median value of the list of matchIndex, this value is the committedIndex since, by definition, half of the * matchIndex values are greater and half are less than this value. So, at least half of the replicas have stored the * median value, this is the definition of committed. */ private void updateCommitted() { List<ReplicaManager> sorted = newArrayList(managers.values()); Collections.sort(sorted, new Comparator<ReplicaManager>() { @Override public int compare(ReplicaManager o, ReplicaManager o2) { return Longs.compare(o.getMatchIndex(), o2.getMatchIndex()); } }); final int middle = (int) Math.ceil(sorted.size() / 2.0); final long committed = sorted.get(middle).getMatchIndex(); log.commitIndex(committed); } private void checkTermOnResponse(RaftStateContext ctx, AppendEntriesResponse response) { if (response.getTerm() > log.currentTerm()) { log.currentTerm(response.getTerm()); stepDown(ctx); } } /** * Notify the {@link ReplicaManager} to send an update the next possible time it can * * @return futures with the result of the update */ @Nonnull @VisibleForTesting List<ListenableFuture<AppendEntriesResponse>> sendRequests(final RaftStateContext ctx) { List<ListenableFuture<AppendEntriesResponse>> responses = newArrayList(); for (ReplicaManager replicaManager : managers.values()) { ListenableFuture<AppendEntriesResponse> response = replicaManager.requestUpdate(); responses.add(response); Futures.addCallback(response, new FutureCallback<AppendEntriesResponse>() { @Override public void onSuccess(@Nullable AppendEntriesResponse result) { updateCommitted(); checkTermOnResponse(ctx, result); } @Override public void onFailure(Throwable t) { } }); } return responses; } }