org.robotninjas.barge.state.Leader.java Source code

Java tutorial

Introduction

Here is the source code for org.robotninjas.barge.state.Leader.java

Source

/**
 * 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;
    }

}