]> source.dussan.org Git - jgit.git/commitdiff
Ketch: Basic replication system 06/64206/6
authorShawn Pearce <spearce@spearce.org>
Mon, 18 Jan 2016 18:33:31 +0000 (10:33 -0800)
committerShawn Pearce <spearce@spearce.org>
Wed, 20 Jan 2016 07:03:32 +0000 (23:03 -0800)
Git Ketch is a multi-master Git repository management system.  Writes
are successful only if a majority of participant servers agree.  Acked
writes are durable against server failures as a majority of the
participants store all required objects.

Git Ketch is modeled on the Raft Consensus Algorithm[1].  A ketch
sailing vessel is faster and more nimble than a raft.  It can also
carry more source codes.

Git Ketch front-loads replication costs, which vaguely resembles a
ketch sailing vessel's distinguishing feature of the main mast on the
front of the ship.

[1] https://raft.github.io/
Change-Id: Ib378dab068961fc7de624cd96030266660b64fb4

22 files changed:
org.eclipse.jgit/META-INF/MANIFEST.MF
org.eclipse.jgit/resources/org/eclipse/jgit/internal/ketch/KetchText.properties [new file with mode: 0644]
org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ElectionRound.java [new file with mode: 0644]
org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchConstants.java [new file with mode: 0644]
org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchLeader.java [new file with mode: 0644]
org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchReplica.java [new file with mode: 0644]
org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchSystem.java [new file with mode: 0644]
org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchText.java [new file with mode: 0644]
org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/LagCheck.java [new file with mode: 0644]
org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/LeaderSnapshot.java [new file with mode: 0644]
org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/LocalReplica.java [new file with mode: 0644]
org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/LogIndex.java [new file with mode: 0644]
org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/Proposal.java [new file with mode: 0644]
org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ProposalRound.java [new file with mode: 0644]
org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/RemoteGitReplica.java [new file with mode: 0644]
org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ReplicaConfig.java [new file with mode: 0644]
org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ReplicaFetchRequest.java [new file with mode: 0644]
org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ReplicaPushRequest.java [new file with mode: 0644]
org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ReplicaSnapshot.java [new file with mode: 0644]
org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/Round.java [new file with mode: 0644]
org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/StageBuilder.java [new file with mode: 0644]
org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/package-info.java [new file with mode: 0644]

index 25d0be6ec7cb75d4e538327c17197648967cfb16..3d3e74f5fdae332fd103946e7dfa263a7291bc41 100644 (file)
@@ -59,6 +59,7 @@ Export-Package: org.eclipse.jgit.annotations;version="4.2.0",
  org.eclipse.jgit.ignore;version="4.2.0",
  org.eclipse.jgit.ignore.internal;version="4.2.0";x-friends:="org.eclipse.jgit.test",
  org.eclipse.jgit.internal;version="4.2.0";x-friends:="org.eclipse.jgit.test,org.eclipse.jgit.http.test",
+ org.eclipse.jgit.internal.ketch;version="4.2.0";x-friends:="org.eclipse.jgit.junit,org.eclipse.jgit.test,org.eclipse.jgit.pgm",
  org.eclipse.jgit.internal.storage.dfs;version="4.2.0";x-friends:="org.eclipse.jgit.test,org.eclipse.jgit.http.server",
  org.eclipse.jgit.internal.storage.file;version="4.2.0";
   x-friends:="org.eclipse.jgit.test,
diff --git a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/ketch/KetchText.properties b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/ketch/KetchText.properties
new file mode 100644 (file)
index 0000000..4984dda
--- /dev/null
@@ -0,0 +1,7 @@
+cannotFetchFromLocalReplica=cannot fetch from LocalReplica
+leaderFailedToStore=leader failed to store
+localReplicaRequired=LocalReplica instance is required
+mismatchedTxnNamespace=mismatched txnNamespace; expected {0} found {1}
+outsideTxnNamespace=ref {0} is outside of txnNamespace {1}
+queuedProposalFailedToApply=queued proposal failed to apply
+unsupportedVoterCount=unsupported voter count {0}, expected one of {1}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ElectionRound.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ElectionRound.java
new file mode 100644 (file)
index 0000000..014eab2
--- /dev/null
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2016, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *   copyright notice, this list of conditions and the following
+ *   disclaimer in the documentation and/or other materials provided
+ *   with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *   names of its contributors may be used to endorse or promote
+ *   products derived from this software without specific prior
+ *   written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.internal.ketch;
+
+import static org.eclipse.jgit.internal.ketch.KetchConstants.TERM;
+
+import java.io.IOException;
+import java.util.List;
+
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.TreeFormatter;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The initial {@link Round} for a leaderless repository, used to establish a
+ * leader.
+ */
+class ElectionRound extends Round {
+       private static final Logger log = LoggerFactory.getLogger(ElectionRound.class);
+
+       private long term;
+
+       ElectionRound(KetchLeader leader, LogIndex head) {
+               super(leader, head);
+       }
+
+       @Override
+       void start() throws IOException {
+               ObjectId id;
+               try (Repository git = leader.openRepository();
+                               ObjectInserter inserter = git.newObjectInserter()) {
+                       id = bumpTerm(git, inserter);
+                       inserter.flush();
+               }
+               runAsync(id);
+       }
+
+       @Override
+       void success() {
+               // Do nothing upon election, KetchLeader will copy the term.
+       }
+
+       long getTerm() {
+               return term;
+       }
+
+       private ObjectId bumpTerm(Repository git, ObjectInserter inserter)
+                       throws IOException {
+               CommitBuilder b = new CommitBuilder();
+               if (!ObjectId.zeroId().equals(acceptedOldIndex)) {
+                       try (RevWalk rw = new RevWalk(git)) {
+                               RevCommit c = rw.parseCommit(acceptedOldIndex);
+                               b.setTreeId(c.getTree());
+                               b.setParentId(acceptedOldIndex);
+                               term = parseTerm(c.getFooterLines(TERM)) + 1;
+                       }
+               } else {
+                       term = 1;
+                       b.setTreeId(inserter.insert(new TreeFormatter()));
+               }
+
+               StringBuilder msg = new StringBuilder();
+               msg.append(KetchConstants.TERM.getName())
+                               .append(": ") //$NON-NLS-1$
+                               .append(term);
+
+               String tag = leader.getSystem().newLeaderTag();
+               if (tag != null && !tag.isEmpty()) {
+                       msg.append(' ').append(tag);
+               }
+
+               b.setAuthor(leader.getSystem().newCommitter());
+               b.setCommitter(b.getAuthor());
+               b.setMessage(msg.toString());
+
+               if (log.isDebugEnabled()) {
+                       log.debug("Trying to elect myself " + b.getMessage()); //$NON-NLS-1$
+               }
+               return inserter.insert(b);
+       }
+
+       private static long parseTerm(List<String> footer) {
+               if (footer.isEmpty()) {
+                       return 0;
+               }
+
+               String s = footer.get(0);
+               int p = s.indexOf(' ');
+               if (p > 0) {
+                       s = s.substring(0, p);
+               }
+               return Long.parseLong(s, 10);
+       }
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchConstants.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchConstants.java
new file mode 100644 (file)
index 0000000..171c059
--- /dev/null
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2016, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *   copyright notice, this list of conditions and the following
+ *   disclaimer in the documentation and/or other materials provided
+ *   with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *   names of its contributors may be used to endorse or promote
+ *   products derived from this software without specific prior
+ *   written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.internal.ketch;
+
+import org.eclipse.jgit.revwalk.FooterKey;
+
+/** Frequently used constants in a Ketch system. */
+public class KetchConstants {
+       /**
+        * Default reference namespace holding {@link #ACCEPTED} and
+        * {@link #COMMITTED} references and the {@link #STAGE} sub-namespace.
+        */
+       public static final String DEFAULT_TXN_NAMESPACE = "refs/txn/"; //$NON-NLS-1$
+
+       /** Reference name holding the RefTree accepted by a follower. */
+       public static final String ACCEPTED = "accepted"; //$NON-NLS-1$
+
+       /** Reference name holding the RefTree known to be committed. */
+       public static final String COMMITTED = "committed"; //$NON-NLS-1$
+
+       /** Reference subdirectory holding proposed heads. */
+       public static final String STAGE = "stage/"; //$NON-NLS-1$
+
+       /** Footer containing the current term. */
+       public static final FooterKey TERM = new FooterKey("Term"); //$NON-NLS-1$
+
+       /** Section for Ketch configuration ({@code ketch}). */
+       public static final String CONFIG_SECTION_KETCH = "ketch"; //$NON-NLS-1$
+
+       /** Behavior for a replica ({@code remote.$name.ketch-type}) */
+       public static final String CONFIG_KEY_TYPE = "ketch-type"; //$NON-NLS-1$
+
+       /** Behavior for a replica ({@code remote.$name.ketch-commit}) */
+       public static final String CONFIG_KEY_COMMIT = "ketch-commit"; //$NON-NLS-1$
+
+       /** Behavior for a replica ({@code remote.$name.ketch-speed}) */
+       public static final String CONFIG_KEY_SPEED = "ketch-speed"; //$NON-NLS-1$
+
+       private KetchConstants() {
+       }
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchLeader.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchLeader.java
new file mode 100644 (file)
index 0000000..851d433
--- /dev/null
@@ -0,0 +1,624 @@
+/*
+ * Copyright (C) 2016, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *   copyright notice, this list of conditions and the following
+ *   disclaimer in the documentation and/or other materials provided
+ *   with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *   names of its contributors may be used to endorse or promote
+ *   products derived from this software without specific prior
+ *   written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.internal.ketch;
+
+import static org.eclipse.jgit.internal.ketch.KetchLeader.State.CANDIDATE;
+import static org.eclipse.jgit.internal.ketch.KetchLeader.State.LEADER;
+import static org.eclipse.jgit.internal.ketch.KetchLeader.State.SHUTDOWN;
+import static org.eclipse.jgit.internal.ketch.KetchReplica.Participation.FOLLOWER_ONLY;
+import static org.eclipse.jgit.internal.ketch.Proposal.State.QUEUED;
+
+import java.io.IOException;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+import org.eclipse.jgit.internal.storage.reftree.RefTree;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A leader managing consensus across remote followers.
+ * <p>
+ * A leader instance starts up in {@link State#CANDIDATE} and tries to begin a
+ * new term by sending an {@link ElectionRound} to all replicas. Its term starts
+ * if a majority of replicas have accepted this leader instance for the term.
+ * <p>
+ * Once elected by a majority the instance enters {@link State#LEADER} and runs
+ * proposals offered to {@link #queueProposal(Proposal)}. This continues until
+ * the leader is timed out for inactivity, or is deposed by a competing leader
+ * gaining its own majority.
+ * <p>
+ * Once timed out or deposed this {@code KetchLeader} instance should be
+ * discarded, and a new instance takes over.
+ * <p>
+ * Each leader instance coordinates a group of {@link KetchReplica}s. Replica
+ * instances are owned by the leader instance and must be discarded when the
+ * leader is discarded.
+ * <p>
+ * In Ketch all push requests are issued through the leader. The steps are as
+ * follows:
+ * <ul>
+ * <li>Create a {@link Proposal} with the
+ * {@link org.eclipse.jgit.transport.ReceiveCommand}s that represent the push.
+ * <li>Invoke {@link #queueProposal(Proposal)} on the leader instance.
+ * <li>Wait for consensus with {@link Proposal#await()}.
+ * <li>To examine the status of the push, check {@link Proposal#getCommands()},
+ * looking at
+ * {@link org.eclipse.jgit.internal.storage.reftree.Command#getResult()}.
+ * </ul>
+ * <p>
+ * The leader gains consensus by first pushing the needed objects and a
+ * {@link RefTree} representing the desired target repository state to the
+ * {@code refs/txn/accepted} branch on each of the replicas. Once a majority has
+ * succeeded, the leader commits the state by either pushing the
+ * {@code refs/txn/accepted} value to {@code refs/txn/committed} (for
+ * Ketch-aware replicas) or by pushing updates to {@code refs/heads/master},
+ * etc. for stock Git replicas.
+ * <p>
+ * Internally, the actual transport to replicas is performed on background
+ * threads via the {@link KetchSystem}'s executor service. For performance, the
+ * {@link KetchLeader}, {@link KetchReplica} and {@link Proposal} objects share
+ * some state, and may invoke each other's methods on different threads. This
+ * access is protected by the leader's {@link #lock} object. Care must be taken
+ * to prevent concurrent access by correctly obtaining the leader's lock.
+ */
+public abstract class KetchLeader {
+       private static final Logger log = LoggerFactory.getLogger(KetchLeader.class);
+
+       /** Current state of the leader instance. */
+       public static enum State {
+               /** Newly created instance trying to elect itself leader. */
+               CANDIDATE,
+
+               /** Leader instance elected by a majority. */
+               LEADER,
+
+               /** Instance has been deposed by another with a more recent term. */
+               DEPOSED,
+
+               /** Leader has been gracefully shutdown, e.g. due to inactivity. */
+               SHUTDOWN;
+       }
+
+       private final KetchSystem system;
+
+       /** Leader's knowledge of replicas for this repository. */
+       private KetchReplica[] voters;
+       private KetchReplica[] followers;
+       private LocalReplica self;
+
+       /**
+        * Lock protecting all data within this leader instance.
+        * <p>
+        * This lock extends into the {@link KetchReplica} instances used by the
+        * leader. They share the same lock instance to simplify concurrency.
+        */
+       final Lock lock;
+
+       private State state = CANDIDATE;
+
+       /** Term of this leader, once elected. */
+       private long term;
+
+       /**
+        * Pending proposals accepted into the queue in FIFO order.
+        * <p>
+        * These proposals were preflighted and do not contain any conflicts with
+        * each other and their expectations matched the leader's local view of the
+        * agreed upon {@code refs/txn/accepted} tree.
+        */
+       private final List<Proposal> queued;
+
+       /**
+        * State of the repository's RefTree after applying all entries in
+        * {@link #queued}. New proposals must be consistent with this tree to be
+        * appended to the end of {@link #queued}.
+        * <p>
+        * Must be deep-copied with {@link RefTree#copy()} if
+        * {@link #roundHoldsReferenceToRefTree} is {@code true}.
+        */
+       private RefTree refTree;
+
+       /**
+        * If {@code true} {@link #refTree} must be duplicated before queuing the
+        * next proposal. The {@link #refTree} was passed into the constructor of a
+        * {@link ProposalRound}, and that external reference to the {@link RefTree}
+        * object is held by the proposal until it materializes the tree object in
+        * the object store. This field is set {@code true} when the proposal begins
+        * execution and set {@code false} once tree objects are persisted in the
+        * local repository's object store or {@link #refTree} is replaced with a
+        * copy to isolate it from any running rounds.
+        * <p>
+        * If proposals arrive less frequently than the {@code RefTree} is written
+        * out to the repository the {@link #roundHoldsReferenceToRefTree} behavior
+        * avoids duplicating {@link #refTree}, reducing both time and memory used.
+        * However if proposals arrive more frequently {@link #refTree} must be
+        * duplicated to prevent newly queued proposals from corrupting the
+        * {@link #runningRound}.
+        */
+       volatile boolean roundHoldsReferenceToRefTree;
+
+       /** End of the leader's log. */
+       private LogIndex headIndex;
+
+       /** Leader knows this (and all prior) states are committed. */
+       private LogIndex committedIndex;
+
+       /**
+        * Is the leader idle with no work pending? If {@code true} there is no work
+        * for the leader (normal state). This field is {@code false} when the
+        * leader thread is scheduled for execution, or while {@link #runningRound}
+        * defines a round in progress.
+        */
+       private boolean idle;
+
+       /** Current round the leader is preparing and waiting for a vote on. */
+       private Round runningRound;
+
+       /**
+        * Construct a leader for a Ketch instance.
+        *
+        * @param system
+        *            Ketch system configuration the leader must adhere to.
+        */
+       protected KetchLeader(KetchSystem system) {
+               this.system = system;
+               this.lock = new ReentrantLock(true /* fair */);
+               this.queued = new ArrayList<>(4);
+               this.idle = true;
+       }
+
+       /** @return system configuration. */
+       KetchSystem getSystem() {
+               return system;
+       }
+
+       /**
+        * Configure the replicas used by this Ketch instance.
+        * <p>
+        * Replicas should be configured once at creation before any proposals are
+        * executed. Once elections happen, <b>reconfiguration is a complicated
+        * concept that is not currently supported</b>.
+        *
+        * @param replicas
+        *            members participating with the same repository.
+        */
+       public void setReplicas(Collection<KetchReplica> replicas) {
+               List<KetchReplica> v = new ArrayList<>(5);
+               List<KetchReplica> f = new ArrayList<>(5);
+               for (KetchReplica r : replicas) {
+                       switch (r.getParticipation()) {
+                       case FULL:
+                               v.add(r);
+                               break;
+
+                       case FOLLOWER_ONLY:
+                               f.add(r);
+                               break;
+                       }
+               }
+
+               Collection<Integer> validVoters = validVoterCounts();
+               if (!validVoters.contains(Integer.valueOf(v.size()))) {
+                       throw new IllegalArgumentException(MessageFormat.format(
+                                       KetchText.get().unsupportedVoterCount,
+                                       Integer.valueOf(v.size()),
+                                       validVoters));
+               }
+
+               LocalReplica me = findLocal(v);
+               if (me == null) {
+                       throw new IllegalArgumentException(
+                                       KetchText.get().localReplicaRequired);
+               }
+
+               lock.lock();
+               try {
+                       voters = v.toArray(new KetchReplica[v.size()]);
+                       followers = f.toArray(new KetchReplica[f.size()]);
+                       self = me;
+               } finally {
+                       lock.unlock();
+               }
+       }
+
+       private static Collection<Integer> validVoterCounts() {
+               @SuppressWarnings("boxing")
+               Integer[] valid = {
+                               // An odd number of voting replicas is required.
+                               1, 3, 5, 7, 9 };
+               return Arrays.asList(valid);
+       }
+
+       private static LocalReplica findLocal(Collection<KetchReplica> voters) {
+               for (KetchReplica r : voters) {
+                       if (r instanceof LocalReplica) {
+                               return (LocalReplica) r;
+                       }
+               }
+               return null;
+       }
+
+       /**
+        * Get an instance of the repository for use by a leader thread.
+        * <p>
+        * The caller will close the repository.
+        *
+        * @return opened repository for use by the leader thread.
+        * @throws IOException
+        *             cannot reopen the repository for the leader.
+        */
+       protected abstract Repository openRepository() throws IOException;
+
+       /**
+        * Queue a reference update proposal for consensus.
+        * <p>
+        * This method does not wait for consensus to be reached. The proposal is
+        * checked to look for risks of conflicts, and then submitted into the queue
+        * for distribution as soon as possible.
+        * <p>
+        * Callers must use {@link Proposal#await()} to see if the proposal is done.
+        *
+        * @param proposal
+        *            the proposed reference updates to queue for consideration.
+        *            Once execution is complete the individual reference result
+        *            fields will be populated with the outcome.
+        * @throws InterruptedException
+        *             current thread was interrupted. The proposal may have been
+        *             aborted if it was not yet queued for execution.
+        * @throws IOException
+        *             unrecoverable error preventing proposals from being attempted
+        *             by this leader.
+        */
+       public void queueProposal(Proposal proposal)
+                       throws InterruptedException, IOException {
+               try {
+                       lock.lockInterruptibly();
+               } catch (InterruptedException e) {
+                       proposal.abort();
+                       throw e;
+               }
+               try {
+                       if (refTree == null) {
+                               initialize();
+                               for (Proposal p : queued) {
+                                       refTree.apply(p.getCommands());
+                               }
+                       } else if (roundHoldsReferenceToRefTree) {
+                               refTree = refTree.copy();
+                               roundHoldsReferenceToRefTree = false;
+                       }
+
+                       if (!refTree.apply(proposal.getCommands())) {
+                               // A conflict exists so abort the proposal.
+                               proposal.abort();
+                               return;
+                       }
+
+                       queued.add(proposal);
+                       proposal.notifyState(QUEUED);
+
+                       if (idle) {
+                               scheduleLeader();
+                       }
+               } finally {
+                       lock.unlock();
+               }
+       }
+
+       private void initialize() throws IOException {
+               try (Repository git = openRepository(); RevWalk rw = new RevWalk(git)) {
+                       self.initialize(git);
+
+                       ObjectId accepted = self.getTxnAccepted();
+                       if (!ObjectId.zeroId().equals(accepted)) {
+                               RevCommit c = rw.parseCommit(accepted);
+                               headIndex = LogIndex.unknown(accepted);
+                               refTree = RefTree.read(rw.getObjectReader(), c.getTree());
+                       } else {
+                               headIndex = LogIndex.unknown(ObjectId.zeroId());
+                               refTree = RefTree.newEmptyTree();
+                       }
+               }
+       }
+
+       private void scheduleLeader() {
+               idle = false;
+               system.getExecutor().execute(new Runnable() {
+                       @Override
+                       public void run() {
+                               runLeader();
+                       }
+               });
+       }
+
+       private void runLeader() {
+               Round round;
+               lock.lock();
+               try {
+                       switch (state) {
+                       case CANDIDATE:
+                               round = new ElectionRound(this, headIndex);
+                               break;
+
+                       case LEADER:
+                               round = newProposalRound();
+                               break;
+
+                       case DEPOSED:
+                       case SHUTDOWN:
+                       default:
+                               log.warn("Leader cannot run {}", state); //$NON-NLS-1$
+                               // TODO(sop): Redirect proposals.
+                               return;
+                       }
+               } finally {
+                       lock.unlock();
+               }
+
+               try {
+                       round.start();
+               } catch (IOException e) {
+                       // TODO(sop) Depose leader if it cannot use its repository.
+                       log.error(KetchText.get().leaderFailedToStore, e);
+                       lock.lock();
+                       try {
+                               nextRound();
+                       } finally {
+                               lock.unlock();
+                       }
+               }
+       }
+
+       private ProposalRound newProposalRound() {
+               List<Proposal> todo = new ArrayList<>(queued);
+               queued.clear();
+               roundHoldsReferenceToRefTree = true;
+               return new ProposalRound(this, headIndex, todo, refTree);
+       }
+
+       /** @return term of this leader's reign. */
+       long getTerm() {
+               return term;
+       }
+
+       /** @return end of the leader's log. */
+       LogIndex getHead() {
+               return headIndex;
+       }
+
+       /**
+        * @return state leader knows it has committed across a quorum of replicas.
+        */
+       LogIndex getCommitted() {
+               return committedIndex;
+       }
+
+       boolean isIdle() {
+               return idle;
+       }
+
+       void runAsync(Round round) {
+               lock.lock();
+               try {
+                       // End of the log is this round. Once transport begins it is
+                       // reasonable to assume at least one replica will eventually get
+                       // this, and there is reasonable probability it commits.
+                       headIndex = round.acceptedNewIndex;
+                       runningRound = round;
+
+                       for (KetchReplica replica : voters) {
+                               replica.pushTxnAcceptedAsync(round);
+                       }
+                       for (KetchReplica replica : followers) {
+                               replica.pushTxnAcceptedAsync(round);
+                       }
+               } finally {
+                       lock.unlock();
+               }
+       }
+
+       /**
+        * Asynchronous signal from a replica after completion.
+        * <p>
+        * Must be called while {@link #lock} is held by the replica.
+        *
+        * @param replica
+        *            replica posting a completion event.
+        */
+       void onReplicaUpdate(KetchReplica replica) {
+               if (log.isDebugEnabled()) {
+                       log.debug("Replica {} finished:\n{}", //$NON-NLS-1$
+                                       replica.describeForLog(), snapshot());
+               }
+
+               if (replica.getParticipation() == FOLLOWER_ONLY) {
+                       // Followers cannot vote, so votes haven't changed.
+                       return;
+               } else if (runningRound == null) {
+                       // No round running, no need to tally votes.
+                       return;
+               }
+
+               assert headIndex.equals(runningRound.acceptedNewIndex);
+               int matching = 0;
+               for (KetchReplica r : voters) {
+                       if (r.hasAccepted(headIndex)) {
+                               matching++;
+                       }
+               }
+
+               int quorum = voters.length / 2 + 1;
+               boolean success = matching >= quorum;
+               if (!success) {
+                       return;
+               }
+
+               switch (state) {
+               case CANDIDATE:
+                       term = ((ElectionRound) runningRound).getTerm();
+                       state = LEADER;
+                       if (log.isDebugEnabled()) {
+                               log.debug("Won election, running term " + term); //$NON-NLS-1$
+                       }
+
+                       //$FALL-THROUGH$
+               case LEADER:
+                       committedIndex = headIndex;
+                       if (log.isDebugEnabled()) {
+                               log.debug("Committed {} in term {}", //$NON-NLS-1$
+                                               committedIndex.describeForLog(),
+                                               Long.valueOf(term));
+                       }
+                       nextRound();
+                       commitAsync(replica);
+                       notifySuccess(runningRound);
+                       if (log.isDebugEnabled()) {
+                               log.debug("Leader state:\n{}", snapshot()); //$NON-NLS-1$
+                       }
+                       break;
+
+               default:
+                       log.debug("Leader ignoring replica while in {}", state); //$NON-NLS-1$
+                       break;
+               }
+       }
+
+       private void notifySuccess(Round round) {
+               // Drop the leader lock while notifying Proposal listeners.
+               lock.unlock();
+               try {
+                       round.success();
+               } finally {
+                       lock.lock();
+               }
+       }
+
+       private void commitAsync(KetchReplica caller) {
+               for (KetchReplica r : voters) {
+                       if (r == caller) {
+                               continue;
+                       }
+                       if (r.shouldPushUnbatchedCommit(committedIndex, isIdle())) {
+                               r.pushCommitAsync(committedIndex);
+                       }
+               }
+               for (KetchReplica r : followers) {
+                       if (r == caller) {
+                               continue;
+                       }
+                       if (r.shouldPushUnbatchedCommit(committedIndex, isIdle())) {
+                               r.pushCommitAsync(committedIndex);
+                       }
+               }
+       }
+
+       /** Schedule the next round; invoked while {@link #lock} is held. */
+       void nextRound() {
+               runningRound = null;
+
+               if (queued.isEmpty()) {
+                       idle = true;
+               } else {
+                       // Caller holds lock. Reschedule leader on a new thread so
+                       // the call stack can unwind and lock is not held unexpectedly
+                       // during prepare for the next round.
+                       scheduleLeader();
+               }
+       }
+
+       /** @return snapshot this leader. */
+       public LeaderSnapshot snapshot() {
+               lock.lock();
+               try {
+                       LeaderSnapshot s = new LeaderSnapshot();
+                       s.state = state;
+                       s.term = term;
+                       s.headIndex = headIndex;
+                       s.committedIndex = committedIndex;
+                       s.idle = isIdle();
+                       for (KetchReplica r : voters) {
+                               s.replicas.add(r.snapshot());
+                       }
+                       for (KetchReplica r : followers) {
+                               s.replicas.add(r.snapshot());
+                       }
+                       return s;
+               } finally {
+                       lock.unlock();
+               }
+       }
+
+       /** Gracefully shutdown this leader and cancel outstanding operations. */
+       public void shutdown() {
+               lock.lock();
+               try {
+                       if (state != SHUTDOWN) {
+                               state = SHUTDOWN;
+                               for (KetchReplica r : voters) {
+                                       r.shutdown();
+                               }
+                               for (KetchReplica r : followers) {
+                                       r.shutdown();
+                               }
+                       }
+               } finally {
+                       lock.unlock();
+               }
+       }
+
+       @Override
+       public String toString() {
+               return snapshot().toString();
+       }
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchReplica.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchReplica.java
new file mode 100644 (file)
index 0000000..a30bbb2
--- /dev/null
@@ -0,0 +1,755 @@
+/*
+ * Copyright (C) 2016, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *   copyright notice, this list of conditions and the following
+ *   disclaimer in the documentation and/or other materials provided
+ *   with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *   names of its contributors may be used to endorse or promote
+ *   products derived from this software without specific prior
+ *   written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.internal.ketch;
+
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static org.eclipse.jgit.internal.ketch.KetchReplica.CommitSpeed.BATCHED;
+import static org.eclipse.jgit.internal.ketch.KetchReplica.CommitSpeed.FAST;
+import static org.eclipse.jgit.internal.ketch.KetchReplica.State.CURRENT;
+import static org.eclipse.jgit.internal.ketch.KetchReplica.State.LAGGING;
+import static org.eclipse.jgit.internal.ketch.KetchReplica.State.OFFLINE;
+import static org.eclipse.jgit.internal.ketch.KetchReplica.State.UNKNOWN;
+import static org.eclipse.jgit.lib.Constants.HEAD;
+import static org.eclipse.jgit.lib.FileMode.TYPE_GITLINK;
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.NOT_ATTEMPTED;
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.OK;
+import static org.eclipse.jgit.transport.ReceiveCommand.Type.CREATE;
+
+import java.io.IOException;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Callable;
+import java.util.concurrent.Future;
+
+import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.internal.storage.reftree.RefTree;
+import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.ReceiveCommand;
+import org.eclipse.jgit.treewalk.TreeWalk;
+import org.eclipse.jgit.util.SystemReader;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A Ketch replica, either {@link LocalReplica} or {@link RemoteGitReplica}.
+ * <p>
+ * Replicas can be either a stock Git replica, or a Ketch-aware replica.
+ * <p>
+ * A stock Git replica has no special knowledge of Ketch and simply stores
+ * objects and references. Ketch communicates with the stock Git replica using
+ * the Git push wire protocol. The {@link KetchLeader} commits an agreed upon
+ * state by pushing all references to the Git replica, for example
+ * {@code "refs/heads/master"} is pushed during commit. Stock Git replicas use
+ * {@link CommitMethod#ALL_REFS} to record the final state.
+ * <p>
+ * Ketch-aware replicas understand the {@code RefTree} sent during the proposal
+ * and during commit are able to update their own reference space to match the
+ * state represented by the {@code RefTree}. Ketch-aware replicas typically use
+ * a {@link org.eclipse.jgit.internal.storage.reftree.RefTreeDatabase} and
+ * {@link CommitMethod#TXN_COMMITTED} to record the final state.
+ * <p>
+ * KetchReplica instances are tightly coupled with a single {@link KetchLeader}.
+ * Some state may be accessed by the leader thread and uses the leader's own
+ * {@link KetchLeader#lock} to protect shared data.
+ */
+public abstract class KetchReplica {
+       static final Logger log = LoggerFactory.getLogger(KetchReplica.class);
+       private static final byte[] PEEL = { ' ', '^' };
+
+       /** Participation of a replica in establishing consensus. */
+       public enum Participation {
+               /** Replica can vote. */
+               FULL,
+
+               /** Replica does not vote, but tracks leader. */
+               FOLLOWER_ONLY;
+       }
+
+       /** How this replica wants to receive Ketch commit operations. */
+       public enum CommitMethod {
+               /** All references are pushed to the peer as standard Git. */
+               ALL_REFS,
+
+               /** Only {@code refs/txn/committed} is written/updated. */
+               TXN_COMMITTED;
+       }
+
+       /** Delay before committing to a replica. */
+       public enum CommitSpeed {
+               /**
+                * Send the commit immediately, even if it could be batched with the
+                * next proposal.
+                */
+               FAST,
+
+               /**
+                * If the next proposal is available, batch the commit with it,
+                * otherwise just send the commit. This generates less network use, but
+                * may provide slower consistency on the replica.
+                */
+               BATCHED;
+       }
+
+       /** Current state of a replica. */
+       public enum State {
+               /** Leader has not yet contacted the replica. */
+               UNKNOWN,
+
+               /** Replica is behind the consensus. */
+               LAGGING,
+
+               /** Replica matches the consensus. */
+               CURRENT,
+
+               /** Replica has a different (or unknown) history. */
+               DIVERGENT,
+
+               /** Replica's history contains the leader's history. */
+               AHEAD,
+
+               /** Replica can not be contacted. */
+               OFFLINE;
+       }
+
+       private final KetchLeader leader;
+       private final String replicaName;
+       private final Participation participation;
+       private final CommitMethod commitMethod;
+       private final CommitSpeed commitSpeed;
+       private final long minRetryMillis;
+       private final long maxRetryMillis;
+       private final Map<ObjectId, List<ReceiveCommand>> staged;
+       private final Map<String, ReceiveCommand> running;
+       private final Map<String, ReceiveCommand> waiting;
+       private final List<ReplicaPushRequest> queued;
+
+       /**
+        * Value known for {@code "refs/txn/accepted"}.
+        * <p>
+        * Raft literature refers to this as {@code matchIndex}.
+        */
+       private ObjectId txnAccepted;
+
+       /**
+        * Value known for {@code "refs/txn/committed"}.
+        * <p>
+        * Raft literature refers to this as {@code commitIndex}. In traditional
+        * Raft this is a state variable inside the follower implementation, but
+        * Ketch keeps it in the leader.
+        */
+       private ObjectId txnCommitted;
+
+       /** What is happening with this replica. */
+       private State state = UNKNOWN;
+       private String error;
+
+       /** Scheduled retry due to communication failure. */
+       private Future<?> retryFuture;
+       private long lastRetryMillis;
+       private long retryAtMillis;
+
+       /**
+        * Configure a replica representation.
+        *
+        * @param leader
+        *            instance this replica follows.
+        * @param name
+        *            unique-ish name identifying this replica for debugging.
+        * @param cfg
+        *            how Ketch should treat the replica.
+        */
+       protected KetchReplica(KetchLeader leader, String name, ReplicaConfig cfg) {
+               this.leader = leader;
+               this.replicaName = name;
+               this.participation = cfg.getParticipation();
+               this.commitMethod = cfg.getCommitMethod();
+               this.commitSpeed = cfg.getCommitSpeed();
+               this.minRetryMillis = cfg.getMinRetry(MILLISECONDS);
+               this.maxRetryMillis = cfg.getMaxRetry(MILLISECONDS);
+               this.staged = new HashMap<>();
+               this.running = new HashMap<>();
+               this.waiting = new HashMap<>();
+               this.queued = new ArrayList<>(4);
+       }
+
+       /** @return system configuration. */
+       public KetchSystem getSystem() {
+               return getLeader().getSystem();
+       }
+
+       /** @return leader instance this replica follows. */
+       public KetchLeader getLeader() {
+               return leader;
+       }
+
+       /** @return unique-ish name for debugging. */
+       public String getName() {
+               return replicaName;
+       }
+
+       /** @return description of this replica for error/debug logging purposes. */
+       protected String describeForLog() {
+               return getName();
+       }
+
+       /** @return how the replica participates in this Ketch system. */
+       public Participation getParticipation() {
+               return participation;
+       }
+
+       /** @return how Ketch will commit to the repository. */
+       public CommitMethod getCommitMethod() {
+               return commitMethod;
+       }
+
+       /** @return when Ketch will commit to the repository. */
+       public CommitSpeed getCommitSpeed() {
+               return commitSpeed;
+       }
+
+       /**
+        * Called by leader to perform graceful shutdown.
+        * <p>
+        * Default implementation cancels any scheduled retry. Subclasses may add
+        * additional logic before or after calling {@code super.shutdown()}.
+        * <p>
+        * Called with {@link KetchLeader#lock} held by caller.
+        */
+       protected void shutdown() {
+               Future<?> f = retryFuture;
+               if (f != null) {
+                       retryFuture = null;
+                       f.cancel(true);
+               }
+       }
+
+       ReplicaSnapshot snapshot() {
+               ReplicaSnapshot s = new ReplicaSnapshot(this);
+               s.accepted = txnAccepted;
+               s.committed = txnCommitted;
+               s.state = state;
+               s.error = error;
+               s.retryAtMillis = waitingForRetry() ? retryAtMillis : 0;
+               return s;
+       }
+
+       /**
+        * Update the leader's view of the replica after a poll.
+        * <p>
+        * Called with {@link KetchLeader#lock} held by caller.
+        *
+        * @param refs
+        *            map of refs from the replica.
+        */
+       void initialize(Map<String, Ref> refs) {
+               if (txnAccepted == null) {
+                       txnAccepted = getId(refs.get(getSystem().getTxnAccepted()));
+               }
+               if (txnCommitted == null) {
+                       txnCommitted = getId(refs.get(getSystem().getTxnCommitted()));
+               }
+       }
+
+       ObjectId getTxnAccepted() {
+               return txnAccepted;
+       }
+
+       boolean hasAccepted(LogIndex id) {
+               return equals(txnAccepted, id);
+       }
+
+       private static boolean equals(@Nullable ObjectId a, LogIndex b) {
+               return a != null && b != null && AnyObjectId.equals(a, b);
+       }
+
+       /**
+        * Schedule a proposal round with the replica.
+        * <p>
+        * Called with {@link KetchLeader#lock} held by caller.
+        *
+        * @param round
+        *            current round being run by the leader.
+        */
+       void pushTxnAcceptedAsync(Round round) {
+               List<ReceiveCommand> cmds = new ArrayList<>();
+               if (commitSpeed == BATCHED) {
+                       LogIndex committedIndex = leader.getCommitted();
+                       if (equals(txnAccepted, committedIndex)
+                                       && !equals(txnCommitted, committedIndex)) {
+                               prepareTxnCommitted(cmds, committedIndex);
+                       }
+               }
+
+               // TODO(sop) Lagging replicas should build accept on the fly.
+               if (round.stageCommands != null) {
+                       for (ReceiveCommand cmd : round.stageCommands) {
+                               // TODO(sop): Do not send certain object graphs to replica.
+                               cmds.add(copy(cmd));
+                       }
+               }
+               cmds.add(new ReceiveCommand(
+                               round.acceptedOldIndex, round.acceptedNewIndex,
+                               getSystem().getTxnAccepted()));
+               pushAsync(new ReplicaPushRequest(this, cmds));
+       }
+
+       private static ReceiveCommand copy(ReceiveCommand c) {
+               return new ReceiveCommand(c.getOldId(), c.getNewId(), c.getRefName());
+       }
+
+       boolean shouldPushUnbatchedCommit(LogIndex committed, boolean leaderIdle) {
+               return (leaderIdle || commitSpeed == FAST) && hasAccepted(committed);
+       }
+
+       void pushCommitAsync(LogIndex committed) {
+               List<ReceiveCommand> cmds = new ArrayList<>();
+               prepareTxnCommitted(cmds, committed);
+               pushAsync(new ReplicaPushRequest(this, cmds));
+       }
+
+       private void prepareTxnCommitted(List<ReceiveCommand> cmds,
+                       ObjectId committed) {
+               removeStaged(cmds, committed);
+               cmds.add(new ReceiveCommand(
+                               txnCommitted, committed,
+                               getSystem().getTxnCommitted()));
+       }
+
+       private void removeStaged(List<ReceiveCommand> cmds, ObjectId committed) {
+               List<ReceiveCommand> a = staged.remove(committed);
+               if (a != null) {
+                       delete(cmds, a);
+               }
+               if (staged.isEmpty() || !(committed instanceof LogIndex)) {
+                       return;
+               }
+
+               LogIndex committedIndex = (LogIndex) committed;
+               Iterator<Map.Entry<ObjectId, List<ReceiveCommand>>> itr = staged
+                               .entrySet().iterator();
+               while (itr.hasNext()) {
+                       Map.Entry<ObjectId, List<ReceiveCommand>> e = itr.next();
+                       if (e.getKey() instanceof LogIndex) {
+                               LogIndex stagedIndex = (LogIndex) e.getKey();
+                               if (stagedIndex.isBefore(committedIndex)) {
+                                       delete(cmds, e.getValue());
+                                       itr.remove();
+                               }
+                       }
+               }
+       }
+
+       private static void delete(List<ReceiveCommand> cmds,
+                       List<ReceiveCommand> createCmds) {
+               for (ReceiveCommand cmd : createCmds) {
+                       ObjectId id = cmd.getNewId();
+                       String name = cmd.getRefName();
+                       cmds.add(new ReceiveCommand(id, ObjectId.zeroId(), name));
+               }
+       }
+
+       /**
+        * Determine the next push for this replica (if any) and start it.
+        * <p>
+        * If the replica has successfully accepted the committed state of the
+        * leader, this method will push all references to the replica using the
+        * configured {@link CommitMethod}.
+        * <p>
+        * If the replica is {@link State#LAGGING} this method will begin catch up
+        * by sending a more recent {@code refs/txn/accepted}.
+        * <p>
+        * Must be invoked with {@link KetchLeader#lock} held by caller.
+        */
+       private void runNextPushRequest() {
+               LogIndex committed = leader.getCommitted();
+               if (!equals(txnCommitted, committed)
+                               && shouldPushUnbatchedCommit(committed, leader.isIdle())) {
+                       pushCommitAsync(committed);
+               }
+
+               if (queued.isEmpty() || !running.isEmpty() || waitingForRetry()) {
+                       return;
+               }
+
+               // Collapse all queued requests into a single request.
+               Map<String, ReceiveCommand> cmdMap = new HashMap<>();
+               for (ReplicaPushRequest req : queued) {
+                       for (ReceiveCommand cmd : req.getCommands()) {
+                               String name = cmd.getRefName();
+                               ReceiveCommand old = cmdMap.remove(name);
+                               if (old != null) {
+                                       cmd = new ReceiveCommand(
+                                                       old.getOldId(), cmd.getNewId(),
+                                                       name);
+                               }
+                               cmdMap.put(name, cmd);
+                       }
+               }
+               queued.clear();
+               waiting.clear();
+
+               List<ReceiveCommand> next = new ArrayList<>(cmdMap.values());
+               for (ReceiveCommand cmd : next) {
+                       running.put(cmd.getRefName(), cmd);
+               }
+               startPush(new ReplicaPushRequest(this, next));
+       }
+
+       private void pushAsync(ReplicaPushRequest req) {
+               if (defer(req)) {
+                       // TODO(sop) Collapse during long retry outage.
+                       for (ReceiveCommand cmd : req.getCommands()) {
+                               waiting.put(cmd.getRefName(), cmd);
+                       }
+                       queued.add(req);
+               } else {
+                       for (ReceiveCommand cmd : req.getCommands()) {
+                               running.put(cmd.getRefName(), cmd);
+                       }
+                       startPush(req);
+               }
+       }
+
+       private boolean defer(ReplicaPushRequest req) {
+               if (waitingForRetry()) {
+                       // Prior communication failure; everything is deferred.
+                       return true;
+               }
+
+               for (ReceiveCommand nextCmd : req.getCommands()) {
+                       ReceiveCommand priorCmd = waiting.get(nextCmd.getRefName());
+                       if (priorCmd == null) {
+                               priorCmd = running.get(nextCmd.getRefName());
+                       }
+                       if (priorCmd != null) {
+                               // Another request pending on same ref; that must go first.
+                               // Verify priorCmd.newId == nextCmd.oldId?
+                               return true;
+                       }
+               }
+               return false;
+       }
+
+       private boolean waitingForRetry() {
+               Future<?> f = retryFuture;
+               return f != null && !f.isDone();
+       }
+
+       private void retryLater(ReplicaPushRequest req) {
+               Collection<ReceiveCommand> cmds = req.getCommands();
+               for (ReceiveCommand cmd : cmds) {
+                       cmd.setResult(NOT_ATTEMPTED, null);
+                       if (!waiting.containsKey(cmd.getRefName())) {
+                               waiting.put(cmd.getRefName(), cmd);
+                       }
+               }
+               queued.add(0, new ReplicaPushRequest(this, cmds));
+
+               if (!waitingForRetry()) {
+                       long delay = KetchSystem.delay(
+                                       lastRetryMillis,
+                                       minRetryMillis, maxRetryMillis);
+                       if (log.isDebugEnabled()) {
+                               log.debug("Retrying {} after {} ms", //$NON-NLS-1$
+                                               describeForLog(), Long.valueOf(delay));
+                       }
+                       lastRetryMillis = delay;
+                       retryAtMillis = SystemReader.getInstance().getCurrentTime() + delay;
+                       retryFuture = getSystem().getExecutor()
+                                       .schedule(new WeakRetryPush(this), delay, MILLISECONDS);
+               }
+       }
+
+       /** Weakly holds a retrying replica, allowing it to garbage collect. */
+       static class WeakRetryPush extends WeakReference<KetchReplica>
+                       implements Callable<Void> {
+               WeakRetryPush(KetchReplica r) {
+                       super(r);
+               }
+
+               @Override
+               public Void call() throws Exception {
+                       KetchReplica r = get();
+                       if (r != null) {
+                               r.doRetryPush();
+                       }
+                       return null;
+               }
+       }
+
+       private void doRetryPush() {
+               leader.lock.lock();
+               try {
+                       retryFuture = null;
+                       runNextPushRequest();
+               } finally {
+                       leader.lock.unlock();
+               }
+       }
+
+       /**
+        * Begin executing a single push.
+        * <p>
+        * This method must move processing onto another thread.
+        * Called with {@link KetchLeader#lock} held by caller.
+        *
+        * @param req
+        *            the request to send to the replica.
+        */
+       protected abstract void startPush(ReplicaPushRequest req);
+
+       /**
+        * Callback from {@link ReplicaPushRequest} upon success or failure.
+        * <p>
+        * Acquires the {@link KetchLeader#lock} and updates the leader's internal
+        * knowledge about this replica to reflect what has been learned during a
+        * push to the replica. In some cases of divergence this method may take
+        * some time to determine how the replica has diverged; to reduce contention
+        * this is evaluated before acquiring the leader lock.
+        *
+        * @param repo
+        *            local repository instance used by the push thread.
+        * @param req
+        *            push request just attempted.
+        */
+       void afterPush(@Nullable Repository repo, ReplicaPushRequest req) {
+               ReceiveCommand acceptCmd = null;
+               ReceiveCommand commitCmd = null;
+               List<ReceiveCommand> stages = null;
+
+               for (ReceiveCommand cmd : req.getCommands()) {
+                       String name = cmd.getRefName();
+                       if (name.equals(getSystem().getTxnAccepted())) {
+                               acceptCmd = cmd;
+                       } else if (name.equals(getSystem().getTxnCommitted())) {
+                               commitCmd = cmd;
+                       } else if (cmd.getResult() == OK && cmd.getType() == CREATE
+                                       && name.startsWith(getSystem().getTxnStage())) {
+                               if (stages == null) {
+                                       stages = new ArrayList<>();
+                               }
+                               stages.add(cmd);
+                       }
+               }
+
+               State newState = null;
+               ObjectId acceptId = readId(req, acceptCmd);
+               if (repo != null && acceptCmd != null && acceptCmd.getResult() != OK
+                               && req.getException() == null) {
+                       try (LagCheck lag = new LagCheck(this, repo)) {
+                               newState = lag.check(acceptId, acceptCmd);
+                               acceptId = lag.getRemoteId();
+                       }
+               }
+
+               leader.lock.lock();
+               try {
+                       for (ReceiveCommand cmd : req.getCommands()) {
+                               running.remove(cmd.getRefName());
+                       }
+
+                       Throwable err = req.getException();
+                       if (err != null) {
+                               state = OFFLINE;
+                               error = err.toString();
+                               retryLater(req);
+                               leader.onReplicaUpdate(this);
+                               return;
+                       }
+
+                       lastRetryMillis = 0;
+                       error = null;
+                       updateView(req, acceptId, commitCmd);
+
+                       if (acceptCmd != null && acceptCmd.getResult() == OK) {
+                               state = hasAccepted(leader.getHead()) ? CURRENT : LAGGING;
+                               if (stages != null) {
+                                       staged.put(acceptCmd.getNewId(), stages);
+                               }
+                       } else if (newState != null) {
+                               state = newState;
+                       }
+
+                       leader.onReplicaUpdate(this);
+                       runNextPushRequest();
+               } finally {
+                       leader.lock.unlock();
+               }
+       }
+
+       private void updateView(ReplicaPushRequest req, @Nullable ObjectId acceptId,
+                       ReceiveCommand commitCmd) {
+               if (acceptId != null) {
+                       txnAccepted = acceptId;
+               }
+
+               ObjectId committed = readId(req, commitCmd);
+               if (committed != null) {
+                       txnCommitted = committed;
+               } else if (acceptId != null && txnCommitted == null) {
+                       // Initialize during first conversation.
+                       Map<String, Ref> adv = req.getRefs();
+                       if (adv != null) {
+                               Ref refs = adv.get(getSystem().getTxnCommitted());
+                               txnCommitted = getId(refs);
+                       }
+               }
+       }
+
+       @Nullable
+       private static ObjectId readId(ReplicaPushRequest req,
+                       @Nullable ReceiveCommand cmd) {
+               if (cmd == null) {
+                       // Ref was not in the command list, do not trust advertisement.
+                       return null;
+
+               } else if (cmd.getResult() == OK) {
+                       // Currently at newId.
+                       return cmd.getNewId();
+               }
+
+               Map<String, Ref> refs = req.getRefs();
+               return refs != null ? getId(refs.get(cmd.getRefName())) : null;
+       }
+
+       /**
+        * Fetch objects from the remote using the calling thread.
+        * <p>
+        * Called without {@link KetchLeader#lock}.
+        *
+        * @param repo
+        *            local repository to fetch objects into.
+        * @param req
+        *            the request to fetch from a replica.
+        * @throws IOException
+        *             communication with the replica was not possible.
+        */
+       protected abstract void blockingFetch(Repository repo,
+                       ReplicaFetchRequest req) throws IOException;
+
+       /**
+        * Build a list of commands to commit {@link CommitMethod#ALL_REFS}.
+        *
+        * @param git
+        *            local leader repository to read committed state from.
+        * @param current
+        *            all known references in the replica's repository. Typically
+        *            this comes from a push advertisement.
+        * @param committed
+        *            state being pushed to {@code refs/txn/committed}.
+        * @return commands to update during commit.
+        * @throws IOException
+        *             cannot read the committed state.
+        */
+       protected Collection<ReceiveCommand> prepareCommit(Repository git,
+                       Map<String, Ref> current, ObjectId committed) throws IOException {
+               List<ReceiveCommand> delta = new ArrayList<>();
+               Map<String, Ref> remote = new HashMap<>(current);
+               try (RevWalk rw = new RevWalk(git);
+                               TreeWalk tw = new TreeWalk(rw.getObjectReader())) {
+                       tw.setRecursive(true);
+                       tw.addTree(rw.parseCommit(committed).getTree());
+                       while (tw.next()) {
+                               if (tw.getRawMode(0) != TYPE_GITLINK
+                                               || tw.isPathSuffix(PEEL, 2)) {
+                                       // Symbolic references cannot be pushed.
+                                       // Caching peeled values is handled remotely.
+                                       continue;
+                               }
+
+                               // TODO(sop) Do not send certain ref names to replica.
+                               String name = RefTree.refName(tw.getPathString());
+                               Ref oldRef = remote.remove(name);
+                               ObjectId oldId = getId(oldRef);
+                               ObjectId newId = tw.getObjectId(0);
+                               if (!AnyObjectId.equals(oldId, newId)) {
+                                       delta.add(new ReceiveCommand(oldId, newId, name));
+                               }
+                       }
+               }
+
+               // Delete any extra references not in the committed state.
+               for (Ref ref : remote.values()) {
+                       if (canDelete(ref)) {
+                               delta.add(new ReceiveCommand(
+                                       ref.getObjectId(), ObjectId.zeroId(),
+                                       ref.getName()));
+                       }
+               }
+               return delta;
+       }
+
+       boolean canDelete(Ref ref) {
+               String name = ref.getName();
+               if (HEAD.equals(name)) {
+                       return false;
+               }
+               if (name.startsWith(getSystem().getTxnNamespace())) {
+                       return false;
+               }
+               // TODO(sop) Do not delete precious names from replica.
+               return true;
+       }
+
+       @NonNull
+       static ObjectId getId(@Nullable Ref ref) {
+               if (ref != null) {
+                       ObjectId id = ref.getObjectId();
+                       if (id != null) {
+                               return id;
+                       }
+               }
+               return ObjectId.zeroId();
+       }
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchSystem.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchSystem.java
new file mode 100644 (file)
index 0000000..71e872e
--- /dev/null
@@ -0,0 +1,301 @@
+/*
+ * Copyright (C) 2016, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *   copyright notice, this list of conditions and the following
+ *   disclaimer in the documentation and/or other materials provided
+ *   with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *   names of its contributors may be used to endorse or promote
+ *   products derived from this software without specific prior
+ *   written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.internal.ketch;
+
+import static org.eclipse.jgit.internal.ketch.KetchConstants.ACCEPTED;
+import static org.eclipse.jgit.internal.ketch.KetchConstants.COMMITTED;
+import static org.eclipse.jgit.internal.ketch.KetchConstants.CONFIG_KEY_TYPE;
+import static org.eclipse.jgit.internal.ketch.KetchConstants.CONFIG_SECTION_KETCH;
+import static org.eclipse.jgit.internal.ketch.KetchConstants.DEFAULT_TXN_NAMESPACE;
+import static org.eclipse.jgit.internal.ketch.KetchConstants.STAGE;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_NAME;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_REMOTE;
+
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.transport.RemoteConfig;
+import org.eclipse.jgit.transport.URIish;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Ketch system-wide configuration.
+ * <p>
+ * This class provides useful defaults for testing and small proof of concepts.
+ * Full scale installations are expected to subclass and override methods to
+ * provide consistent configuration across all managed repositories.
+ * <p>
+ * Servers should configure their own {@link ScheduledExecutorService}.
+ */
+public class KetchSystem {
+       private static final Random RNG = new Random();
+
+       /** @return default executor, one thread per available processor. */
+       public static ScheduledExecutorService defaultExecutor() {
+               return DefaultExecutorHolder.I;
+       }
+
+       private final ScheduledExecutorService executor;
+       private final String txnNamespace;
+       private final String txnAccepted;
+       private final String txnCommitted;
+       private final String txnStage;
+
+       /** Create a default system with a thread pool of 1 thread per CPU. */
+       public KetchSystem() {
+               this(defaultExecutor(), DEFAULT_TXN_NAMESPACE);
+       }
+
+       /**
+        * Create a Ketch system with the provided executor service.
+        *
+        * @param executor
+        *            thread pool to run background operations.
+        * @param txnNamespace
+        *            reference namespace for the RefTree graph and associated
+        *            transaction state. Must begin with {@code "refs/"} and end
+        *            with {@code '/'}, for example {@code "refs/txn/"}.
+        */
+       public KetchSystem(ScheduledExecutorService executor, String txnNamespace) {
+               this.executor = executor;
+               this.txnNamespace = txnNamespace;
+               this.txnAccepted = txnNamespace + ACCEPTED;
+               this.txnCommitted = txnNamespace + COMMITTED;
+               this.txnStage = txnNamespace + STAGE;
+       }
+
+       /** @return executor to perform background operations. */
+       public ScheduledExecutorService getExecutor() {
+               return executor;
+       }
+
+       /**
+        * Get the namespace used for the RefTree graph and transaction management.
+        *
+        * @return reference namespace such as {@code "refs/txn/"}.
+        */
+       public String getTxnNamespace() {
+               return txnNamespace;
+       }
+
+       /** @return name of the accepted RefTree graph. */
+       public String getTxnAccepted() {
+               return txnAccepted;
+       }
+
+       /** @return name of the committed RefTree graph. */
+       public String getTxnCommitted() {
+               return txnCommitted;
+       }
+
+       /** @return prefix for staged objects, e.g. {@code "refs/txn/stage/"}. */
+       public String getTxnStage() {
+               return txnStage;
+       }
+
+       /** @return identity line for the committer header of a RefTreeGraph. */
+       public PersonIdent newCommitter() {
+               String name = "ketch"; //$NON-NLS-1$
+               String email = "ketch@system"; //$NON-NLS-1$
+               return new PersonIdent(name, email);
+       }
+
+       /**
+        * Construct a random tag to identify a candidate during leader election.
+        * <p>
+        * Multiple processes trying to elect themselves leaders at exactly the same
+        * time (rounded to seconds) using the same {@link #newCommitter()} identity
+        * strings, for the same term, may generate the same ObjectId for the
+        * election commit and falsely assume they have both won.
+        * <p>
+        * Candidates add this tag to their election ballot commit to disambiguate
+        * the election. The tag only needs to be unique for a given triplet of
+        * {@link #newCommitter()}, system time (rounded to seconds), and term. If
+        * every replica in the system uses a unique {@code newCommitter} (such as
+        * including the host name after the {@code "@"} in the email address) the
+        * tag could be the empty string.
+        * <p>
+        * The default implementation generates a few bytes of random data.
+        *
+        * @return unique tag; null or empty string if {@code newCommitter()} is
+        *         sufficiently unique to identify the leader.
+        */
+       @Nullable
+       public String newLeaderTag() {
+               int n = RNG.nextInt(1 << (6 * 4));
+               return String.format("%06x", Integer.valueOf(n)); //$NON-NLS-1$
+       }
+
+       /**
+        * Construct the KetchLeader instance of a repository.
+        *
+        * @param repo
+        *            local repository stored by the leader.
+        * @return leader instance.
+        * @throws URISyntaxException
+        *             a follower configuration contains an unsupported URI.
+        */
+       public KetchLeader createLeader(final Repository repo)
+                       throws URISyntaxException {
+               KetchLeader leader = new KetchLeader(this) {
+                       @Override
+                       protected Repository openRepository() {
+                               repo.incrementOpen();
+                               return repo;
+                       }
+               };
+               leader.setReplicas(createReplicas(leader, repo));
+               return leader;
+       }
+
+       /**
+        * Get the collection of replicas for a repository.
+        * <p>
+        * The collection of replicas must include the local repository.
+        *
+        * @param leader
+        *            the leader driving these replicas.
+        * @param repo
+        *            repository to get the replicas of.
+        * @return collection of replicas for the specified repository.
+        * @throws URISyntaxException
+        *             a configured URI is invalid.
+        */
+       protected List<KetchReplica> createReplicas(KetchLeader leader,
+                       Repository repo) throws URISyntaxException {
+               List<KetchReplica> replicas = new ArrayList<>();
+               Config cfg = repo.getConfig();
+               String localName = getLocalName(cfg);
+               for (String name : cfg.getSubsections(CONFIG_KEY_REMOTE)) {
+                       if (!hasParticipation(cfg, name)) {
+                               continue;
+                       }
+
+                       ReplicaConfig kc = ReplicaConfig.newFromConfig(cfg, name);
+                       if (name.equals(localName)) {
+                               replicas.add(new LocalReplica(leader, name, kc));
+                               continue;
+                       }
+
+                       RemoteConfig rc = new RemoteConfig(cfg, name);
+                       List<URIish> uris = rc.getPushURIs();
+                       if (uris.isEmpty()) {
+                               uris = rc.getURIs();
+                       }
+                       for (URIish uri : uris) {
+                               String n = uris.size() == 1 ? name : uri.getHost();
+                               replicas.add(new RemoteGitReplica(leader, n, uri, kc, rc));
+                       }
+               }
+               return replicas;
+       }
+
+       private static boolean hasParticipation(Config cfg, String name) {
+               return cfg.getString(CONFIG_KEY_REMOTE, name, CONFIG_KEY_TYPE) != null;
+       }
+
+       private static String getLocalName(Config cfg) {
+               return cfg.getString(CONFIG_SECTION_KETCH, null, CONFIG_KEY_NAME);
+       }
+
+       static class DefaultExecutorHolder {
+               private static final Logger log = LoggerFactory.getLogger(KetchSystem.class);
+               static final ScheduledExecutorService I = create();
+
+               private static ScheduledExecutorService create() {
+                       int cores = Runtime.getRuntime().availableProcessors();
+                       int threads = Math.max(5, cores);
+                       log.info("Using {} threads", Integer.valueOf(threads)); //$NON-NLS-1$
+                       return Executors.newScheduledThreadPool(
+                               threads,
+                               new ThreadFactory() {
+                                       private final AtomicInteger threadCnt = new AtomicInteger();
+
+                                       @Override
+                                       public Thread newThread(Runnable r) {
+                                               int id = threadCnt.incrementAndGet();
+                                               Thread thr = new Thread(r);
+                                               thr.setName("KetchExecutor-" + id); //$NON-NLS-1$
+                                               return thr;
+                                       }
+                               });
+               }
+
+               private DefaultExecutorHolder() {
+               }
+       }
+
+       /**
+        * Compute a delay in a {@code min..max} interval with random jitter.
+        *
+        * @param last
+        *            amount of delay waited before the last attempt. This is used
+        *            to seed the next delay interval. Should be 0 if there was no
+        *            prior delay.
+        * @param min
+        *            shortest amount of allowable delay between attempts.
+        * @param max
+        *            longest amount of allowable delay between attempts.
+        * @return new amount of delay to wait before the next attempt.
+        */
+       static long delay(long last, long min, long max) {
+               long r = Math.max(0, last * 3 - min);
+               if (r > 0) {
+                       int c = (int) Math.min(r + 1, Integer.MAX_VALUE);
+                       r = RNG.nextInt(c);
+               }
+               return Math.max(Math.min(min + r, max), min);
+       }
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchText.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchText.java
new file mode 100644 (file)
index 0000000..a4e42bb
--- /dev/null
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2016, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *   copyright notice, this list of conditions and the following
+ *   disclaimer in the documentation and/or other materials provided
+ *   with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *   names of its contributors may be used to endorse or promote
+ *   products derived from this software without specific prior
+ *   written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.internal.ketch;
+
+import org.eclipse.jgit.nls.NLS;
+import org.eclipse.jgit.nls.TranslationBundle;
+
+/** Translation bundle for the Ketch implementation. */
+public class KetchText extends TranslationBundle {
+       /** @return instance of this translation bundle. */
+       public static KetchText get() {
+               return NLS.getBundleFor(KetchText.class);
+       }
+
+       // @formatter:off
+       /***/ public String cannotFetchFromLocalReplica;
+       /***/ public String leaderFailedToStore;
+       /***/ public String localReplicaRequired;
+       /***/ public String mismatchedTxnNamespace;
+       /***/ public String outsideTxnNamespace;
+       /***/ public String queuedProposalFailedToApply;
+       /***/ public String unsupportedVoterCount;
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/LagCheck.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/LagCheck.java
new file mode 100644 (file)
index 0000000..35327ea
--- /dev/null
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2016, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *   copyright notice, this list of conditions and the following
+ *   disclaimer in the documentation and/or other materials provided
+ *   with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *   names of its contributors may be used to endorse or promote
+ *   products derived from this software without specific prior
+ *   written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.internal.ketch;
+
+import static org.eclipse.jgit.internal.ketch.KetchReplica.State.AHEAD;
+import static org.eclipse.jgit.internal.ketch.KetchReplica.State.DIVERGENT;
+import static org.eclipse.jgit.internal.ketch.KetchReplica.State.LAGGING;
+import static org.eclipse.jgit.internal.ketch.KetchReplica.State.UNKNOWN;
+import static org.eclipse.jgit.lib.Constants.OBJ_COMMIT;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Map;
+
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.ReceiveCommand;
+
+/**
+ * A helper to check if a {@link KetchReplica} is ahead or behind the leader.
+ */
+class LagCheck implements AutoCloseable {
+       private final KetchReplica replica;
+       private final Repository repo;
+       private RevWalk rw;
+       private ObjectId remoteId;
+
+       LagCheck(KetchReplica replica, Repository repo) {
+               this.replica = replica;
+               this.repo = repo;
+               initRevWalk();
+       }
+
+       private void initRevWalk() {
+               if (rw != null) {
+                       rw.close();
+               }
+
+               rw = new RevWalk(repo);
+               rw.setRetainBody(false);
+       }
+
+       public void close() {
+               if (rw != null) {
+                       rw.close();
+                       rw = null;
+               }
+       }
+
+       ObjectId getRemoteId() {
+               return remoteId;
+       }
+
+       KetchReplica.State check(ObjectId acceptId, ReceiveCommand acceptCmd) {
+               remoteId = acceptId;
+               if (remoteId == null) {
+                       // Nothing advertised by the replica, value is unknown.
+                       return UNKNOWN;
+               }
+
+               if (AnyObjectId.equals(remoteId, ObjectId.zeroId())) {
+                       // Replica does not have the txnAccepted reference.
+                       return LAGGING;
+               }
+
+               try {
+                       RevCommit remote;
+                       try {
+                               remote = parseRemoteCommit(acceptCmd.getRefName());
+                       } catch (RefGoneException gone) {
+                               // Replica does not have the txnAccepted reference.
+                               return LAGGING;
+                       } catch (MissingObjectException notFound) {
+                               // Local repository does not know this commit so it cannot
+                               // be including the replica's log.
+                               return DIVERGENT;
+                       }
+
+                       RevCommit head = rw.parseCommit(acceptCmd.getNewId());
+                       if (rw.isMergedInto(remote, head)) {
+                               return LAGGING;
+                       }
+
+                       // TODO(sop) Check term to see if my leader was deposed.
+                       if (rw.isMergedInto(head, remote)) {
+                               return AHEAD;
+                       } else {
+                               return DIVERGENT;
+                       }
+               } catch (IOException err) {
+                       KetchReplica.log.error(String.format(
+                                       "Cannot compare %s", //$NON-NLS-1$
+                                       acceptCmd.getRefName()), err);
+                       return UNKNOWN;
+               }
+       }
+
+       private RevCommit parseRemoteCommit(String refName)
+                       throws IOException, MissingObjectException, RefGoneException {
+               try {
+                       return rw.parseCommit(remoteId);
+               } catch (MissingObjectException notLocal) {
+                       // Fall through and try to acquire the object by fetching it.
+               }
+
+               ReplicaFetchRequest fetch = new ReplicaFetchRequest(
+                               Collections.singleton(refName),
+                               Collections.<ObjectId> emptySet());
+               try {
+                       replica.blockingFetch(repo, fetch);
+               } catch (IOException fetchErr) {
+                       KetchReplica.log.error(String.format(
+                                       "Cannot fetch %s (%s) from %s", //$NON-NLS-1$
+                                       remoteId.abbreviate(8).name(), refName,
+                                       replica.describeForLog()), fetchErr);
+                       throw new MissingObjectException(remoteId, OBJ_COMMIT);
+               }
+
+               Map<String, Ref> adv = fetch.getRefs();
+               if (adv == null) {
+                       throw new MissingObjectException(remoteId, OBJ_COMMIT);
+               }
+
+               Ref ref = adv.get(refName);
+               if (ref == null || ref.getObjectId() == null) {
+                       throw new RefGoneException();
+               }
+
+               initRevWalk();
+               remoteId = ref.getObjectId();
+               return rw.parseCommit(remoteId);
+       }
+
+       private static class RefGoneException extends Exception {
+               private static final long serialVersionUID = 1L;
+       }
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/LeaderSnapshot.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/LeaderSnapshot.java
new file mode 100644 (file)
index 0000000..28a49df
--- /dev/null
@@ -0,0 +1,164 @@
+/*
+ * Copyright (C) 2016, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *   copyright notice, this list of conditions and the following
+ *   disclaimer in the documentation and/or other materials provided
+ *   with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *   names of its contributors may be used to endorse or promote
+ *   products derived from this software without specific prior
+ *   written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.internal.ketch;
+
+import static org.eclipse.jgit.internal.ketch.KetchReplica.State.OFFLINE;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.lib.ObjectId;
+
+/** A snapshot of a leader and its view of the world. */
+public class LeaderSnapshot {
+       final List<ReplicaSnapshot> replicas = new ArrayList<>();
+       KetchLeader.State state;
+       long term;
+       LogIndex headIndex;
+       LogIndex committedIndex;
+       boolean idle;
+
+       LeaderSnapshot() {
+       }
+
+       /** @return unmodifiable view of configured replicas. */
+       public Collection<ReplicaSnapshot> getReplicas() {
+               return Collections.unmodifiableList(replicas);
+       }
+
+       /** @return current state of the leader. */
+       public KetchLeader.State getState() {
+               return state;
+       }
+
+       /**
+        * @return {@code true} if the leader is not running a round to reach
+        *         consensus, and has no rounds queued.
+        */
+       public boolean isIdle() {
+               return idle;
+       }
+
+       /**
+        * @return term of this leader. Valid only if {@link #getState()} is
+        *         currently {@link KetchLeader.State#LEADER}.
+        */
+       public long getTerm() {
+               return term;
+       }
+
+       /**
+        * @return end of the leader's log; null if leader hasn't started up enough
+        *         to begin its own election.
+        */
+       @Nullable
+       public LogIndex getHead() {
+               return headIndex;
+       }
+
+       /**
+        * @return state the leader knows is committed on a majority of participant
+        *         replicas. Null until the leader instance has committed a log
+        *         index within its own term.
+        */
+       @Nullable
+       public LogIndex getCommitted() {
+               return committedIndex;
+       }
+
+       @Override
+       public String toString() {
+               StringBuilder s = new StringBuilder();
+               s.append(isIdle() ? "IDLE" : "RUNNING"); //$NON-NLS-1$ //$NON-NLS-2$
+               s.append(" state ").append(getState()); //$NON-NLS-1$
+               if (getTerm() > 0) {
+                       s.append(" term ").append(getTerm()); //$NON-NLS-1$
+               }
+               s.append('\n');
+               s.append(String.format(
+                               "%-10s %12s %12s\n", //$NON-NLS-1$
+                               "Replica", "Accepted", "Committed")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+               s.append("------------------------------------\n"); //$NON-NLS-1$
+               debug(s, "(leader)", getHead(), getCommitted()); //$NON-NLS-1$
+               s.append('\n');
+               for (ReplicaSnapshot r : getReplicas()) {
+                       debug(s, r);
+                       s.append('\n');
+               }
+               s.append('\n');
+               return s.toString();
+       }
+
+       private static void debug(StringBuilder b, ReplicaSnapshot s) {
+               KetchReplica replica = s.getReplica();
+               debug(b, replica.getName(), s.getAccepted(), s.getCommitted());
+               b.append(String.format(" %-8s %s", //$NON-NLS-1$
+                               replica.getParticipation(), s.getState()));
+               if (s.getState() == OFFLINE) {
+                       String err = s.getErrorMessage();
+                       if (err != null) {
+                               b.append(" (").append(err).append(')'); //$NON-NLS-1$
+                       }
+               }
+       }
+
+       private static void debug(StringBuilder s, String name,
+                       ObjectId accepted, ObjectId committed) {
+               s.append(String.format(
+                               "%-10s %-12s %-12s", //$NON-NLS-1$
+                               name, str(accepted), str(committed)));
+       }
+
+       static String str(ObjectId c) {
+               if (c instanceof LogIndex) {
+                       return ((LogIndex) c).describeForLog();
+               } else if (c != null) {
+                       return c.abbreviate(8).name();
+               }
+               return "-"; //$NON-NLS-1$
+       }
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/LocalReplica.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/LocalReplica.java
new file mode 100644 (file)
index 0000000..e297bca
--- /dev/null
@@ -0,0 +1,229 @@
+/*
+ * Copyright (C) 2016, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *   copyright notice, this list of conditions and the following
+ *   disclaimer in the documentation and/or other materials provided
+ *   with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *   names of its contributors may be used to endorse or promote
+ *   products derived from this software without specific prior
+ *   written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.internal.ketch;
+
+import static org.eclipse.jgit.internal.ketch.KetchReplica.CommitMethod.ALL_REFS;
+import static org.eclipse.jgit.internal.ketch.KetchReplica.CommitMethod.TXN_COMMITTED;
+import static org.eclipse.jgit.lib.RefDatabase.ALL;
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.OK;
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_OTHER_REASON;
+
+import java.io.IOException;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.jgit.internal.storage.reftree.RefTreeDatabase;
+import org.eclipse.jgit.lib.BatchRefUpdate;
+import org.eclipse.jgit.lib.NullProgressMonitor;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefDatabase;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.ReceiveCommand;
+
+/** Ketch replica running on the same system as the {@link KetchLeader}. */
+public class LocalReplica extends KetchReplica {
+       /**
+        * Configure a local replica.
+        *
+        * @param leader
+        *            instance this replica follows.
+        * @param name
+        *            unique-ish name identifying this replica for debugging.
+        * @param cfg
+        *            how Ketch should treat the local system.
+        */
+       public LocalReplica(KetchLeader leader, String name, ReplicaConfig cfg) {
+               super(leader, name, cfg);
+       }
+
+       @Override
+       protected String describeForLog() {
+               return String.format("%s (leader)", getName()); //$NON-NLS-1$
+       }
+
+       /**
+        * Initializes local replica by reading accepted and committed references.
+        * <p>
+        * Loads accepted and committed references from the reference database of
+        * the local replica and stores their current ObjectIds in memory.
+        *
+        * @param repo
+        *            repository to initialize state from.
+        * @throws IOException
+        *             cannot read repository state.
+        */
+       void initialize(Repository repo) throws IOException {
+               RefDatabase refdb = repo.getRefDatabase();
+               if (refdb instanceof RefTreeDatabase) {
+                       RefTreeDatabase treeDb = (RefTreeDatabase) refdb;
+                       String txnNamespace = getSystem().getTxnNamespace();
+                       if (!txnNamespace.equals(treeDb.getTxnNamespace())) {
+                               throw new IOException(MessageFormat.format(
+                                               KetchText.get().mismatchedTxnNamespace,
+                                               txnNamespace, treeDb.getTxnNamespace()));
+                       }
+                       refdb = treeDb.getBootstrap();
+               }
+               initialize(refdb.exactRef(
+                               getSystem().getTxnAccepted(),
+                               getSystem().getTxnCommitted()));
+       }
+
+       @Override
+       protected void startPush(final ReplicaPushRequest req) {
+               getSystem().getExecutor().execute(new Runnable() {
+                       @Override
+                       public void run() {
+                               try (Repository git = getLeader().openRepository()) {
+                                       try {
+                                               update(git, req);
+                                               req.done(git);
+                                       } catch (Throwable err) {
+                                               req.setException(git, err);
+                                       }
+                               } catch (IOException err) {
+                                       req.setException(null, err);
+                               }
+                       }
+               });
+       }
+
+       @Override
+       protected void blockingFetch(Repository repo, ReplicaFetchRequest req)
+                       throws IOException {
+               throw new IOException(KetchText.get().cannotFetchFromLocalReplica);
+       }
+
+       private void update(Repository git, ReplicaPushRequest req)
+                       throws IOException {
+               RefDatabase refdb = git.getRefDatabase();
+               CommitMethod method = getCommitMethod();
+
+               // Local replica probably uses RefTreeDatabase, the request should
+               // be only for the txnNamespace, so drop to the bootstrap layer.
+               if (refdb instanceof RefTreeDatabase) {
+                       if (!isOnlyTxnNamespace(req.getCommands())) {
+                               return;
+                       }
+
+                       refdb = ((RefTreeDatabase) refdb).getBootstrap();
+                       method = TXN_COMMITTED;
+               }
+
+               BatchRefUpdate batch = refdb.newBatchUpdate();
+               batch.setRefLogIdent(getSystem().newCommitter());
+               batch.setRefLogMessage("ketch", false); //$NON-NLS-1$
+               batch.setAllowNonFastForwards(true);
+
+               // RefDirectory updates multiple references sequentially.
+               // Run everything else first, then accepted (if present),
+               // then committed (if present). This ensures an earlier
+               // failure will not update these critical references.
+               ReceiveCommand accepted = null;
+               ReceiveCommand committed = null;
+               for (ReceiveCommand cmd : req.getCommands()) {
+                       String name = cmd.getRefName();
+                       if (name.equals(getSystem().getTxnAccepted())) {
+                               accepted = cmd;
+                       } else if (name.equals(getSystem().getTxnCommitted())) {
+                               committed = cmd;
+                       } else {
+                               batch.addCommand(cmd);
+                       }
+               }
+               if (committed != null && method == ALL_REFS) {
+                       Map<String, Ref> refs = refdb.getRefs(ALL);
+                       batch.addCommand(prepareCommit(git, refs, committed.getNewId()));
+               }
+               if (accepted != null) {
+                       batch.addCommand(accepted);
+               }
+               if (committed != null) {
+                       batch.addCommand(committed);
+               }
+
+               try (RevWalk rw = new RevWalk(git)) {
+                       batch.execute(rw, NullProgressMonitor.INSTANCE);
+               }
+
+               // KetchReplica only cares about accepted and committed in
+               // advertisement. If they failed, store the current values
+               // back in the ReplicaPushRequest.
+               List<String> failed = new ArrayList<>(2);
+               checkFailed(failed, accepted);
+               checkFailed(failed, committed);
+               if (!failed.isEmpty()) {
+                       String[] arr = failed.toArray(new String[failed.size()]);
+                       req.setRefs(refdb.exactRef(arr));
+               }
+       }
+
+       private static void checkFailed(List<String> failed, ReceiveCommand cmd) {
+               if (cmd != null && cmd.getResult() != OK) {
+                       failed.add(cmd.getRefName());
+               }
+       }
+
+       private boolean isOnlyTxnNamespace(Collection<ReceiveCommand> cmdList) {
+               // Be paranoid and reject non txnNamespace names, this
+               // is a programming error in Ketch that should not occur.
+
+               String txnNamespace = getSystem().getTxnNamespace();
+               for (ReceiveCommand cmd : cmdList) {
+                       if (!cmd.getRefName().startsWith(txnNamespace)) {
+                               cmd.setResult(REJECTED_OTHER_REASON,
+                                               MessageFormat.format(
+                                                               KetchText.get().outsideTxnNamespace,
+                                                               cmd.getRefName(), txnNamespace));
+                               ReceiveCommand.abort(cmdList);
+                               return false;
+                       }
+               }
+               return true;
+       }
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/LogIndex.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/LogIndex.java
new file mode 100644 (file)
index 0000000..350c8ed
--- /dev/null
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2016, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *   copyright notice, this list of conditions and the following
+ *   disclaimer in the documentation and/or other materials provided
+ *   with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *   names of its contributors may be used to endorse or promote
+ *   products derived from this software without specific prior
+ *   written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.internal.ketch;
+
+import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.ObjectId;
+
+/**
+ * An ObjectId for a commit extended with incrementing log index.
+ * <p>
+ * For any two LogIndex instances, {@code A} is an ancestor of {@code C}
+ * reachable through parent edges in the graph if {@code A.index < C.index}.
+ * LogIndex provides a performance optimization for Ketch, the same information
+ * can be obtained from {@link org.eclipse.jgit.revwalk.RevWalk}.
+ * <p>
+ * Index values are only valid within a single {@link KetchLeader} instance
+ * after it has won an election. By restricting scope to a single leader new
+ * leaders do not need to traverse the entire history to determine the next
+ * {@code index} for new proposals. This differs from Raft, where leader
+ * election uses the log index and the term number to determine which replica
+ * holds a sufficiently up-to-date log. Since Ketch uses Git objects for storage
+ * of its replicated log, it keeps the term number as Raft does but uses
+ * standard Git operations to imply the log index.
+ * <p>
+ * {@link Round#runAsync(AnyObjectId)} bumps the index as each new round is
+ * constructed.
+ */
+public class LogIndex extends ObjectId {
+       static LogIndex unknown(AnyObjectId id) {
+               return new LogIndex(id, 0);
+       }
+
+       private final long index;
+
+       private LogIndex(AnyObjectId id, long index) {
+               super(id);
+               this.index = index;
+       }
+
+       LogIndex nextIndex(AnyObjectId id) {
+               return new LogIndex(id, index + 1);
+       }
+
+       /** @return index provided by the current leader instance. */
+       public long getIndex() {
+               return index;
+       }
+
+       /**
+        * Check if this log position committed before another log position.
+        * <p>
+        * Only valid for log positions in memory for the current leader.
+        *
+        * @param c
+        *            other (more recent) log position.
+        * @return true if this log position was before {@code c} or equal to c and
+        *         therefore any agreement of {@code c} implies agreement on this
+        *         log position.
+        */
+       boolean isBefore(LogIndex c) {
+               return index <= c.index;
+       }
+
+       /**
+        * @return string suitable for debug logging containing the log index and
+        *         abbreviated ObjectId.
+        */
+       @SuppressWarnings("boxing")
+       public String describeForLog() {
+               return String.format("%5d/%s", index, abbreviate(6).name()); //$NON-NLS-1$
+       }
+
+       @SuppressWarnings("boxing")
+       @Override
+       public String toString() {
+               return String.format("LogId[%5d/%s]", index, name()); //$NON-NLS-1$
+       }
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/Proposal.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/Proposal.java
new file mode 100644 (file)
index 0000000..0876eb5
--- /dev/null
@@ -0,0 +1,391 @@
+/*
+ * Copyright (C) 2016, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *   copyright notice, this list of conditions and the following
+ *   disclaimer in the documentation and/or other materials provided
+ *   with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *   names of its contributors may be used to endorse or promote
+ *   products derived from this software without specific prior
+ *   written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.internal.ketch;
+
+import static org.eclipse.jgit.internal.ketch.Proposal.State.ABORTED;
+import static org.eclipse.jgit.internal.ketch.Proposal.State.EXECUTED;
+import static org.eclipse.jgit.internal.ketch.Proposal.State.NEW;
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.NOT_ATTEMPTED;
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.OK;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.internal.storage.reftree.Command;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.PushCertificate;
+import org.eclipse.jgit.transport.ReceiveCommand;
+
+/**
+ * A proposal to be applied in a Ketch system.
+ * <p>
+ * Pushing to a Ketch leader results in the leader making a proposal. The
+ * proposal includes the list of reference updates. The leader attempts to send
+ * the proposal to a quorum of replicas by pushing the proposal to a "staging"
+ * area under the {@code refs/txn/stage/} namespace. If the proposal succeeds
+ * then the changes are durable and the leader can commit the proposal.
+ * <p>
+ * Proposals are executed by {@link KetchLeader#queueProposal(Proposal)}, which
+ * runs them asynchronously in the background. Proposals are thread-safe futures
+ * allowing callers to {@link #await()} for results or be notified by callback
+ * using {@link #addListener(Runnable)}.
+ */
+public class Proposal {
+       /** Current state of the proposal. */
+       public enum State {
+               /** Proposal has not yet been given to a {@link KetchLeader}. */
+               NEW(false),
+
+               /**
+                * Proposal was validated and has entered the queue, but a round
+                * containing this proposal has not started yet.
+                */
+               QUEUED(false),
+
+               /** Round containing the proposal has begun and is in progress. */
+               RUNNING(false),
+
+               /**
+                * Proposal was executed through a round. Individual results from
+                * {@link Proposal#getCommands()}, {@link Command#getResult()} explain
+                * the success or failure outcome.
+                */
+               EXECUTED(true),
+
+               /** Proposal was aborted and did not reach consensus. */
+               ABORTED(true);
+
+               private final boolean done;
+
+               private State(boolean done) {
+                       this.done = done;
+               }
+
+               /** @return true if this is a terminal state. */
+               public boolean isDone() {
+                       return done;
+               }
+       }
+
+       private final List<Command> commands;
+       private PersonIdent author;
+       private String message;
+       private PushCertificate pushCert;
+       private final List<Runnable> listeners = new CopyOnWriteArrayList<>();
+       private final AtomicReference<State> state = new AtomicReference<>(NEW);
+
+       /**
+        * Create a proposal from a list of Ketch commands.
+        *
+        * @param cmds
+        *            prepared list of commands.
+        */
+       public Proposal(List<Command> cmds) {
+               commands = Collections.unmodifiableList(new ArrayList<>(cmds));
+       }
+
+       /**
+        * Create a proposal from a collection of received commands.
+        *
+        * @param rw
+        *            walker to assist in preparing commands.
+        * @param cmds
+        *            list of pending commands.
+        * @throws MissingObjectException
+        *             newId of a command is not found locally.
+        * @throws IOException
+        *             local objects cannot be accessed.
+        */
+       public Proposal(RevWalk rw, Collection<ReceiveCommand> cmds)
+                       throws MissingObjectException, IOException {
+               commands = asCommandList(rw, cmds);
+       }
+
+       private static List<Command> asCommandList(RevWalk rw,
+                       Collection<ReceiveCommand> cmds)
+                                       throws MissingObjectException, IOException {
+               List<Command> commands = new ArrayList<>(cmds.size());
+               for (ReceiveCommand cmd : cmds) {
+                       commands.add(new Command(rw, cmd));
+               }
+               return Collections.unmodifiableList(commands);
+       }
+
+       /** @return commands from this proposal. */
+       public Collection<Command> getCommands() {
+               return commands;
+       }
+
+       /** @return optional author of the proposal. */
+       @Nullable
+       public PersonIdent getAuthor() {
+               return author;
+       }
+
+       /**
+        * Set the author for the proposal.
+        *
+        * @param who
+        *            optional identity of the author of the proposal.
+        * @return {@code this}
+        */
+       public Proposal setAuthor(@Nullable PersonIdent who) {
+               author = who;
+               return this;
+       }
+
+       /** @return optional message for the commit log of the RefTree. */
+       @Nullable
+       public String getMessage() {
+               return message;
+       }
+
+       /**
+        * Set the message to appear in the commit log of the RefTree.
+        *
+        * @param msg
+        *            message text for the commit.
+        * @return {@code this}
+        */
+       public Proposal setMessage(@Nullable String msg) {
+               message = msg != null && !msg.isEmpty() ? msg : null;
+               return this;
+       }
+
+       /** @return optional certificate signing the references. */
+       @Nullable
+       public PushCertificate getPushCertificate() {
+               return pushCert;
+       }
+
+       /**
+        * Set the push certificate signing the references.
+        *
+        * @param cert
+        *            certificate, may be null.
+        * @return {@code this}
+        */
+       public Proposal setPushCertificate(@Nullable PushCertificate cert) {
+               pushCert = cert;
+               return this;
+       }
+
+       /**
+        * Add a callback to be invoked when the proposal is done.
+        * <p>
+        * A proposal is done when it has entered either {@link State#EXECUTED} or
+        * {@link State#ABORTED} state. If the proposal is already done
+        * {@code callback.run()} is immediately invoked on the caller's thread.
+        *
+        * @param callback
+        *            method to run after the proposal is done. The callback may be
+        *            run on a Ketch system thread and should be completed quickly.
+        */
+       public void addListener(Runnable callback) {
+               boolean runNow = false;
+               synchronized (state) {
+                       if (state.get().isDone()) {
+                               runNow = true;
+                       } else {
+                               listeners.add(callback);
+                       }
+               }
+               if (runNow) {
+                       callback.run();
+               }
+       }
+
+       /** Set command result as OK. */
+       void success() {
+               for (Command c : commands) {
+                       if (c.getResult() == NOT_ATTEMPTED) {
+                               c.setResult(OK);
+                       }
+               }
+               notifyState(EXECUTED);
+       }
+
+       /** Mark commands as "transaction aborted". */
+       void abort() {
+               Command.abort(commands, null);
+               notifyState(ABORTED);
+       }
+
+       /** @return read the current state of the proposal. */
+       public State getState() {
+               return state.get();
+       }
+
+       /**
+        * @return {@code true} if the proposal was attempted. A true value does not
+        *         mean consensus was reached, only that the proposal was considered
+        *         and will not be making any more progress beyond its current
+        *         state.
+        */
+       public boolean isDone() {
+               return state.get().isDone();
+       }
+
+       /**
+        * Wait for the proposal to be attempted and {@link #isDone()} to be true.
+        *
+        * @throws InterruptedException
+        *             caller was interrupted before proposal executed.
+        */
+       public void await() throws InterruptedException {
+               synchronized (state) {
+                       while (!state.get().isDone()) {
+                               state.wait();
+                       }
+               }
+       }
+
+       /**
+        * Wait for the proposal to be attempted and {@link #isDone()} to be true.
+        *
+        * @param wait
+        *            how long to wait.
+        * @param unit
+        *            unit describing the wait time.
+        * @return true if the proposal is done; false if the method timed out.
+        * @throws InterruptedException
+        *             caller was interrupted before proposal executed.
+        */
+       public boolean await(long wait, TimeUnit unit) throws InterruptedException {
+               synchronized (state) {
+                       if (state.get().isDone()) {
+                               return true;
+                       }
+                       state.wait(unit.toMillis(wait));
+                       return state.get().isDone();
+               }
+       }
+
+       /**
+        * Wait for the proposal to exit a state.
+        *
+        * @param notIn
+        *            state the proposal should not be in to return.
+        * @param wait
+        *            how long to wait.
+        * @param unit
+        *            unit describing the wait time.
+        * @return true if the proposal exited the state; false on time out.
+        * @throws InterruptedException
+        *             caller was interrupted before proposal executed.
+        */
+       public boolean awaitStateChange(State notIn, long wait, TimeUnit unit)
+                       throws InterruptedException {
+               synchronized (state) {
+                       if (state.get() != notIn) {
+                               return true;
+                       }
+                       state.wait(unit.toMillis(wait));
+                       return state.get() != notIn;
+               }
+       }
+
+       void notifyState(State s) {
+               synchronized (state) {
+                       state.set(s);
+                       state.notifyAll();
+               }
+               if (s.isDone()) {
+                       for (Runnable callback : listeners) {
+                               callback.run();
+                       }
+                       listeners.clear();
+               }
+       }
+
+       @Override
+       public String toString() {
+               StringBuilder s = new StringBuilder();
+               s.append("Ketch Proposal {\n"); //$NON-NLS-1$
+               s.append("  ").append(state.get()).append('\n'); //$NON-NLS-1$
+               if (author != null) {
+                       s.append("  author ").append(author).append('\n'); //$NON-NLS-1$
+               }
+               if (message != null) {
+                       s.append("  message ").append(message).append('\n'); //$NON-NLS-1$
+               }
+               for (Command c : commands) {
+                       s.append("  "); //$NON-NLS-1$
+                       format(s, c.getOldRef(), "CREATE"); //$NON-NLS-1$
+                       s.append(' ');
+                       format(s, c.getNewRef(), "DELETE"); //$NON-NLS-1$
+                       s.append(' ').append(c.getRefName());
+                       if (c.getResult() != ReceiveCommand.Result.NOT_ATTEMPTED) {
+                               s.append(' ').append(c.getResult()); // $NON-NLS-1$
+                       }
+                       s.append('\n');
+               }
+               s.append('}');
+               return s.toString();
+       }
+
+       private static void format(StringBuilder s, @Nullable Ref r, String n) {
+               if (r == null) {
+                       s.append(n);
+               } else if (r.isSymbolic()) {
+                       s.append(r.getTarget().getName());
+               } else {
+                       ObjectId id = r.getObjectId();
+                       if (id != null) {
+                               s.append(id.abbreviate(8).name());
+                       }
+               }
+       }
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ProposalRound.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ProposalRound.java
new file mode 100644 (file)
index 0000000..d34477a
--- /dev/null
@@ -0,0 +1,299 @@
+/*
+ * Copyright (C) 2016, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *   copyright notice, this list of conditions and the following
+ *   disclaimer in the documentation and/or other materials provided
+ *   with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *   names of its contributors may be used to endorse or promote
+ *   products derived from this software without specific prior
+ *   written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.internal.ketch;
+
+import static org.eclipse.jgit.internal.ketch.Proposal.State.RUNNING;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.internal.storage.reftree.Command;
+import org.eclipse.jgit.internal.storage.reftree.RefTree;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.ReceiveCommand;
+
+/** A {@link Round} that aggregates and sends user {@link Proposal}s. */
+class ProposalRound extends Round {
+       private final List<Proposal> todo;
+       private RefTree queuedTree;
+
+       ProposalRound(KetchLeader leader, LogIndex head, List<Proposal> todo,
+                       @Nullable RefTree tree) {
+               super(leader, head);
+               this.todo = todo;
+
+               if (tree != null && canCombine(todo)) {
+                       this.queuedTree = tree;
+               } else {
+                       leader.roundHoldsReferenceToRefTree = false;
+               }
+       }
+
+       private static boolean canCombine(List<Proposal> todo) {
+               Proposal first = todo.get(0);
+               for (int i = 1; i < todo.size(); i++) {
+                       if (!canCombine(first, todo.get(i))) {
+                               return false;
+                       }
+               }
+               return true;
+       }
+
+       private static boolean canCombine(Proposal a, Proposal b) {
+               String aMsg = nullToEmpty(a.getMessage());
+               String bMsg = nullToEmpty(b.getMessage());
+               return aMsg.equals(bMsg) && canCombine(a.getAuthor(), b.getAuthor());
+       }
+
+       private static String nullToEmpty(@Nullable String str) {
+               return str != null ? str : ""; //$NON-NLS-1$
+       }
+
+       private static boolean canCombine(@Nullable PersonIdent a,
+                       @Nullable PersonIdent b) {
+               if (a != null && b != null) {
+                       // Same name and email address. Combine timestamp as the two
+                       // proposals are running concurrently and appear together or
+                       // not at all from the point of view of an outside reader.
+                       return a.getName().equals(b.getName())
+                                       && a.getEmailAddress().equals(b.getEmailAddress());
+               }
+
+               // If a and b are null, both will be the system identity.
+               return a == null && b == null;
+       }
+
+       void start() throws IOException {
+               for (Proposal p : todo) {
+                       p.notifyState(RUNNING);
+               }
+               try {
+                       ObjectId id;
+                       try (Repository git = leader.openRepository()) {
+                               id = insertProposals(git);
+                       }
+                       runAsync(id);
+               } catch (NoOp e) {
+                       for (Proposal p : todo) {
+                               p.success();
+                       }
+                       leader.lock.lock();
+                       try {
+                               leader.nextRound();
+                       } finally {
+                               leader.lock.unlock();
+                       }
+               } catch (IOException e) {
+                       abort();
+                       throw e;
+               }
+       }
+
+       private ObjectId insertProposals(Repository git)
+                       throws IOException, NoOp {
+               ObjectId id;
+               try (ObjectInserter inserter = git.newObjectInserter()) {
+                       // TODO(sop) Process signed push certificates.
+
+                       if (queuedTree != null) {
+                               id = insertSingleProposal(git, inserter);
+                       } else {
+                               id = insertMultiProposal(git, inserter);
+                       }
+
+                       stageCommands = makeStageList(git, inserter);
+                       inserter.flush();
+               }
+               return id;
+       }
+
+       private ObjectId insertSingleProposal(Repository git,
+                       ObjectInserter inserter) throws IOException, NoOp {
+               // Fast path: tree is passed in with all proposals applied.
+               ObjectId treeId = queuedTree.writeTree(inserter);
+               queuedTree = null;
+               leader.roundHoldsReferenceToRefTree = false;
+
+               if (!ObjectId.zeroId().equals(acceptedOldIndex)) {
+                       try (RevWalk rw = new RevWalk(git)) {
+                               RevCommit c = rw.parseCommit(acceptedOldIndex);
+                               if (treeId.equals(c.getTree())) {
+                                       throw new NoOp();
+                               }
+                       }
+               }
+
+               Proposal p = todo.get(0);
+               CommitBuilder b = new CommitBuilder();
+               b.setTreeId(treeId);
+               if (!ObjectId.zeroId().equals(acceptedOldIndex)) {
+                       b.setParentId(acceptedOldIndex);
+               }
+               b.setCommitter(leader.getSystem().newCommitter());
+               b.setAuthor(p.getAuthor() != null ? p.getAuthor() : b.getCommitter());
+               b.setMessage(message(p));
+               return inserter.insert(b);
+       }
+
+       private ObjectId insertMultiProposal(Repository git,
+                       ObjectInserter inserter) throws IOException, NoOp {
+               // The tree was not passed in, or there are multiple proposals
+               // each needing their own commit. Reset the tree and replay each
+               // proposal in order as individual commits.
+               ObjectId lastIndex = acceptedOldIndex;
+               ObjectId oldTreeId;
+               RefTree tree;
+               if (ObjectId.zeroId().equals(lastIndex)) {
+                       oldTreeId = ObjectId.zeroId();
+                       tree = RefTree.newEmptyTree();
+               } else {
+                       try (RevWalk rw = new RevWalk(git)) {
+                               RevCommit c = rw.parseCommit(lastIndex);
+                               oldTreeId = c.getTree();
+                               tree = RefTree.read(rw.getObjectReader(), c.getTree());
+                       }
+               }
+
+               PersonIdent committer = leader.getSystem().newCommitter();
+               for (Proposal p : todo) {
+                       if (!tree.apply(p.getCommands())) {
+                               // This should not occur, previously during queuing the
+                               // commands were successfully applied to the pending tree.
+                               // Abort the entire round.
+                               throw new IOException(
+                                               KetchText.get().queuedProposalFailedToApply);
+                       }
+
+                       ObjectId treeId = tree.writeTree(inserter);
+                       if (treeId.equals(oldTreeId)) {
+                               continue;
+                       }
+
+                       CommitBuilder b = new CommitBuilder();
+                       b.setTreeId(treeId);
+                       if (!ObjectId.zeroId().equals(lastIndex)) {
+                               b.setParentId(lastIndex);
+                       }
+                       b.setAuthor(p.getAuthor() != null ? p.getAuthor() : committer);
+                       b.setCommitter(committer);
+                       b.setMessage(message(p));
+                       lastIndex = inserter.insert(b);
+               }
+               if (lastIndex.equals(acceptedOldIndex)) {
+                       throw new NoOp();
+               }
+               return lastIndex;
+       }
+
+       private String message(Proposal p) {
+               StringBuilder m = new StringBuilder();
+               String msg = p.getMessage();
+               if (msg != null && !msg.isEmpty()) {
+                       m.append(msg);
+                       while (m.length() < 2 || m.charAt(m.length() - 2) != '\n'
+                                       || m.charAt(m.length() - 1) != '\n') {
+                               m.append('\n');
+                       }
+               }
+               m.append(KetchConstants.TERM.getName())
+                               .append(": ") //$NON-NLS-1$
+                               .append(leader.getTerm());
+               return m.toString();
+       }
+
+       void abort() {
+               for (Proposal p : todo) {
+                       p.abort();
+               }
+       }
+
+       void success() {
+               for (Proposal p : todo) {
+                       p.success();
+               }
+       }
+
+       private List<ReceiveCommand> makeStageList(Repository git,
+                       ObjectInserter inserter) throws IOException {
+               // For each branch, collapse consecutive updates to only most recent,
+               // avoiding sending multiple objects in a rapid fast-forward chain, or
+               // rewritten content.
+               Map<String, ObjectId> byRef = new HashMap<>();
+               for (Proposal p : todo) {
+                       for (Command c : p.getCommands()) {
+                               Ref n = c.getNewRef();
+                               if (n != null && !n.isSymbolic()) {
+                                       byRef.put(n.getName(), n.getObjectId());
+                               }
+                       }
+               }
+               if (byRef.isEmpty()) {
+                       return Collections.emptyList();
+               }
+
+               Set<ObjectId> newObjs = new HashSet<>(byRef.values());
+               StageBuilder b = new StageBuilder(
+                               leader.getSystem().getTxnStage(),
+                               acceptedNewIndex);
+               return b.makeStageList(newObjs, git, inserter);
+       }
+
+
+       private static class NoOp extends Exception {
+               private static final long serialVersionUID = 1L;
+       }
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/RemoteGitReplica.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/RemoteGitReplica.java
new file mode 100644 (file)
index 0000000..6f4a178
--- /dev/null
@@ -0,0 +1,316 @@
+/*
+ * Copyright (C) 2016, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *   copyright notice, this list of conditions and the following
+ *   disclaimer in the documentation and/or other materials provided
+ *   with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *   names of its contributors may be used to endorse or promote
+ *   products derived from this software without specific prior
+ *   written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.internal.ketch;
+
+import static org.eclipse.jgit.internal.ketch.KetchReplica.CommitMethod.ALL_REFS;
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.LOCK_FAILURE;
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.NOT_ATTEMPTED;
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.OK;
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_NODELETE;
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_NONFASTFORWARD;
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_OTHER_REASON;
+import static org.eclipse.jgit.lib.Ref.Storage.NETWORK;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.errors.NotSupportedException;
+import org.eclipse.jgit.errors.TransportException;
+import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.NullProgressMonitor;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectIdRef;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.transport.FetchConnection;
+import org.eclipse.jgit.transport.PushConnection;
+import org.eclipse.jgit.transport.ReceiveCommand;
+import org.eclipse.jgit.transport.RemoteConfig;
+import org.eclipse.jgit.transport.RemoteRefUpdate;
+import org.eclipse.jgit.transport.Transport;
+import org.eclipse.jgit.transport.URIish;
+
+/**
+ * Representation of a Git repository on a remote replica system.
+ * <p>
+ * {@link KetchLeader} will contact the replica using the Git wire protocol.
+ * <p>
+ * The remote replica may be fully Ketch-aware, or a standard Git server.
+ */
+public class RemoteGitReplica extends KetchReplica {
+       private final URIish uri;
+       private final RemoteConfig remoteConfig;
+
+       /**
+        * Configure a new remote.
+        *
+        * @param leader
+        *            instance this replica follows.
+        * @param name
+        *            unique-ish name identifying this remote for debugging.
+        * @param uri
+        *            URI to connect to the follower's repository.
+        * @param cfg
+        *            how Ketch should treat the remote system.
+        * @param rc
+        *            optional remote configuration describing how to contact the
+        *            peer repository.
+        */
+       public RemoteGitReplica(KetchLeader leader, String name, URIish uri,
+                       ReplicaConfig cfg, @Nullable RemoteConfig rc) {
+               super(leader, name, cfg);
+               this.uri = uri;
+               this.remoteConfig = rc;
+       }
+
+       /** @return URI to contact the remote peer repository. */
+       public URIish getURI() {
+               return uri;
+       }
+
+       /** @return optional configuration describing how to contact the peer. */
+       @Nullable
+       protected RemoteConfig getRemoteConfig() {
+               return remoteConfig;
+       }
+
+       @Override
+       protected String describeForLog() {
+               return String.format("%s @ %s", getName(), getURI()); //$NON-NLS-1$
+       }
+
+       @Override
+       protected void startPush(final ReplicaPushRequest req) {
+               getSystem().getExecutor().execute(new Runnable() {
+                       @Override
+                       public void run() {
+                               try (Repository git = getLeader().openRepository()) {
+                                       try {
+                                               push(git, req);
+                                               req.done(git);
+                                       } catch (Throwable err) {
+                                               req.setException(git, err);
+                                       }
+                               } catch (IOException err) {
+                                       req.setException(null, err);
+                               }
+                       }
+               });
+       }
+
+       private void push(Repository repo, ReplicaPushRequest req)
+                       throws NotSupportedException, TransportException, IOException {
+               Map<String, Ref> adv;
+               List<RemoteCommand> cmds = asUpdateList(req.getCommands());
+               try (Transport transport = Transport.open(repo, uri)) {
+                       RemoteConfig rc = getRemoteConfig();
+                       if (rc != null) {
+                               transport.applyConfig(rc);
+                       }
+                       transport.setPushAtomic(true);
+                       adv = push(repo, transport, cmds);
+               }
+               for (RemoteCommand c : cmds) {
+                       c.copyStatusToResult();
+               }
+               req.setRefs(adv);
+       }
+
+       private Map<String, Ref> push(Repository git, Transport transport,
+                       List<RemoteCommand> cmds) throws IOException {
+               Map<String, RemoteRefUpdate> updates = asUpdateMap(cmds);
+               try (PushConnection connection = transport.openPush()) {
+                       Map<String, Ref> adv = connection.getRefsMap();
+                       RemoteRefUpdate accepted = updates.get(getSystem().getTxnAccepted());
+                       if (accepted != null && !isExpectedValue(adv, accepted)) {
+                               abort(cmds);
+                               return adv;
+                       }
+
+                       RemoteRefUpdate committed = updates.get(getSystem().getTxnCommitted());
+                       if (committed != null && !isExpectedValue(adv, committed)) {
+                               abort(cmds);
+                               return adv;
+                       }
+                       if (committed != null && getCommitMethod() == ALL_REFS) {
+                               prepareCommit(git, cmds, updates, adv,
+                                               committed.getNewObjectId());
+                       }
+
+                       connection.push(NullProgressMonitor.INSTANCE, updates);
+                       return adv;
+               }
+       }
+
+       private static boolean isExpectedValue(Map<String, Ref> adv,
+                       RemoteRefUpdate u) {
+               Ref r = adv.get(u.getRemoteName());
+               if (!AnyObjectId.equals(getId(r), u.getExpectedOldObjectId())) {
+                       ((RemoteCommand) u).cmd.setResult(LOCK_FAILURE);
+                       return false;
+               }
+               return true;
+       }
+
+       private void prepareCommit(Repository git, List<RemoteCommand> cmds,
+                       Map<String, RemoteRefUpdate> updates, Map<String, Ref> adv,
+                       ObjectId committed) throws IOException {
+               for (ReceiveCommand cmd : prepareCommit(git, adv, committed)) {
+                       RemoteCommand c = new RemoteCommand(cmd);
+                       cmds.add(c);
+                       updates.put(c.getRemoteName(), c);
+               }
+       }
+
+       private static List<RemoteCommand> asUpdateList(
+                       Collection<ReceiveCommand> cmds) {
+               try {
+                       List<RemoteCommand> toPush = new ArrayList<>(cmds.size());
+                       for (ReceiveCommand cmd : cmds) {
+                               toPush.add(new RemoteCommand(cmd));
+                       }
+                       return toPush;
+               } catch (IOException e) {
+                       // Cannot occur as no IO was required to build the command.
+                       throw new IllegalStateException(e);
+               }
+       }
+
+       private static Map<String, RemoteRefUpdate> asUpdateMap(
+                       List<RemoteCommand> cmds) {
+               Map<String, RemoteRefUpdate> m = new LinkedHashMap<>();
+               for (RemoteCommand cmd : cmds) {
+                       m.put(cmd.getRemoteName(), cmd);
+               }
+               return m;
+       }
+
+       private static void abort(List<RemoteCommand> cmds) {
+               List<ReceiveCommand> tmp = new ArrayList<>(cmds.size());
+               for (RemoteCommand cmd : cmds) {
+                       tmp.add(cmd.cmd);
+               }
+               ReceiveCommand.abort(tmp);
+       }
+
+       protected void blockingFetch(Repository repo, ReplicaFetchRequest req)
+                       throws NotSupportedException, TransportException {
+               try (Transport transport = Transport.open(repo, uri)) {
+                       RemoteConfig rc = getRemoteConfig();
+                       if (rc != null) {
+                               transport.applyConfig(rc);
+                       }
+                       fetch(transport, req);
+               }
+       }
+
+       private void fetch(Transport transport, ReplicaFetchRequest req)
+                       throws NotSupportedException, TransportException {
+               try (FetchConnection conn = transport.openFetch()) {
+                       Map<String, Ref> remoteRefs = conn.getRefsMap();
+                       req.setRefs(remoteRefs);
+
+                       List<Ref> want = new ArrayList<>();
+                       for (String name : req.getWantRefs()) {
+                               Ref ref = remoteRefs.get(name);
+                               if (ref != null && ref.getObjectId() != null) {
+                                       want.add(ref);
+                               }
+                       }
+                       for (ObjectId id : req.getWantObjects()) {
+                               want.add(new ObjectIdRef.Unpeeled(NETWORK, id.name(), id));
+                       }
+
+                       conn.fetch(NullProgressMonitor.INSTANCE, want,
+                                       Collections.<ObjectId> emptySet());
+               }
+       }
+
+       static class RemoteCommand extends RemoteRefUpdate {
+               final ReceiveCommand cmd;
+
+               RemoteCommand(ReceiveCommand cmd) throws IOException {
+                       super(null, null,
+                                       cmd.getNewId(), cmd.getRefName(),
+                                       true /* force update */,
+                                       null /* no local tracking ref */,
+                                       cmd.getOldId());
+                       this.cmd = cmd;
+               }
+
+               void copyStatusToResult() {
+                       if (cmd.getResult() == NOT_ATTEMPTED) {
+                               switch (getStatus()) {
+                               case OK:
+                               case UP_TO_DATE:
+                               case NON_EXISTING:
+                                       cmd.setResult(OK);
+                                       break;
+
+                               case REJECTED_NODELETE:
+                                       cmd.setResult(REJECTED_NODELETE);
+                                       break;
+
+                               case REJECTED_NONFASTFORWARD:
+                                       cmd.setResult(REJECTED_NONFASTFORWARD);
+                                       break;
+
+                               case REJECTED_OTHER_REASON:
+                                       cmd.setResult(REJECTED_OTHER_REASON, getMessage());
+                                       break;
+
+                               default:
+                                       cmd.setResult(REJECTED_OTHER_REASON, getStatus().name());
+                                       break;
+                               }
+                       }
+               }
+       }
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ReplicaConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ReplicaConfig.java
new file mode 100644 (file)
index 0000000..e16e63a
--- /dev/null
@@ -0,0 +1,233 @@
+/*
+ * Copyright (C) 2016, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *   copyright notice, this list of conditions and the following
+ *   disclaimer in the documentation and/or other materials provided
+ *   with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *   names of its contributors may be used to endorse or promote
+ *   products derived from this software without specific prior
+ *   written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.internal.ketch;
+
+import static java.util.concurrent.TimeUnit.DAYS;
+import static java.util.concurrent.TimeUnit.HOURS;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.concurrent.TimeUnit.MINUTES;
+import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.eclipse.jgit.internal.ketch.KetchConstants.CONFIG_KEY_COMMIT;
+import static org.eclipse.jgit.internal.ketch.KetchConstants.CONFIG_KEY_SPEED;
+import static org.eclipse.jgit.internal.ketch.KetchConstants.CONFIG_KEY_TYPE;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_REMOTE;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.eclipse.jgit.internal.ketch.KetchReplica.CommitMethod;
+import org.eclipse.jgit.internal.ketch.KetchReplica.CommitSpeed;
+import org.eclipse.jgit.internal.ketch.KetchReplica.Participation;
+import org.eclipse.jgit.lib.Config;
+
+/** Configures a {@link KetchReplica}. */
+public class ReplicaConfig {
+       /**
+        * Read a configuration from a config block.
+        *
+        * @param cfg
+        *            configuration to read.
+        * @param name
+        *            of the replica being configured.
+        * @return replica configuration for {@code name}.
+        */
+       public static ReplicaConfig newFromConfig(Config cfg, String name) {
+               return new ReplicaConfig().fromConfig(cfg, name);
+       }
+
+       private Participation participation = Participation.FULL;
+       private CommitMethod commitMethod = CommitMethod.ALL_REFS;
+       private CommitSpeed commitSpeed = CommitSpeed.BATCHED;
+       private long minRetry = SECONDS.toMillis(5);
+       private long maxRetry = MINUTES.toMillis(1);
+
+       /** @return participation of the replica in the system. */
+       public Participation getParticipation() {
+               return participation;
+       }
+
+       /** @return how Ketch should apply committed changes. */
+       public CommitMethod getCommitMethod() {
+               return commitMethod;
+       }
+
+       /** @return how quickly should Ketch commit. */
+       public CommitSpeed getCommitSpeed() {
+               return commitSpeed;
+       }
+
+       /**
+        * Returns the minimum wait delay before retrying a failure.
+        *
+        * @param unit
+        *            to get retry delay in.
+        * @return minimum delay before retrying a failure.
+        */
+       public long getMinRetry(TimeUnit unit) {
+               return unit.convert(minRetry, MILLISECONDS);
+       }
+
+       /**
+        * Returns the maximum wait delay before retrying a failure.
+        *
+        * @param unit
+        *            to get retry delay in.
+        * @return maximum delay before retrying a failure.
+        */
+       public long getMaxRetry(TimeUnit unit) {
+               return unit.convert(maxRetry, MILLISECONDS);
+       }
+
+       /**
+        * Update the configuration from a config block.
+        *
+        * @param cfg
+        *            configuration to read.
+        * @param name
+        *            of the replica being configured.
+        * @return {@code this}
+        */
+       public ReplicaConfig fromConfig(Config cfg, String name) {
+               participation = cfg.getEnum(
+                               CONFIG_KEY_REMOTE, name, CONFIG_KEY_TYPE,
+                               participation);
+               commitMethod = cfg.getEnum(
+                               CONFIG_KEY_REMOTE, name, CONFIG_KEY_COMMIT,
+                               commitMethod);
+               commitSpeed = cfg.getEnum(
+                               CONFIG_KEY_REMOTE, name, CONFIG_KEY_SPEED,
+                               commitSpeed);
+               minRetry = getMillis(cfg, name, "ketch-minRetry", minRetry); //$NON-NLS-1$
+               maxRetry = getMillis(cfg, name, "ketch-maxRetry", maxRetry); //$NON-NLS-1$
+               return this;
+       }
+
+       private static long getMillis(Config cfg, String name, String key,
+                       long defaultValue) {
+               String valStr = cfg.getString(CONFIG_KEY_REMOTE, name, key);
+               if (valStr == null) {
+                       return defaultValue;
+               }
+
+               valStr = valStr.trim();
+               if (valStr.isEmpty()) {
+                       return defaultValue;
+               }
+
+               Matcher m = UnitMap.PATTERN.matcher(valStr);
+               if (!m.matches()) {
+                       return defaultValue;
+               }
+
+               String digits = m.group(1);
+               String unitName = m.group(2).trim();
+               TimeUnit unit = UnitMap.UNITS.get(unitName);
+               if (unit == null) {
+                       return defaultValue;
+               }
+
+               try {
+                       if (digits.indexOf('.') == -1) {
+                               return unit.toMillis(Long.parseLong(digits));
+                       }
+
+                       double val = Double.parseDouble(digits);
+                       return (long) (val * unit.toMillis(1));
+               } catch (NumberFormatException nfe) {
+                       return defaultValue;
+               }
+       }
+
+       static class UnitMap {
+               static final Pattern PATTERN = Pattern
+                               .compile("^([1-9][0-9]*(?:\\.[0-9]*)?)\\s*(.*)$"); //$NON-NLS-1$
+
+               static final Map<String, TimeUnit> UNITS;
+
+               static {
+                       Map<String, TimeUnit> m = new HashMap<>();
+                       TimeUnit u = MILLISECONDS;
+                       m.put("", u); //$NON-NLS-1$
+                       m.put("ms", u); //$NON-NLS-1$
+                       m.put("millis", u); //$NON-NLS-1$
+                       m.put("millisecond", u); //$NON-NLS-1$
+                       m.put("milliseconds", u); //$NON-NLS-1$
+
+                       u = SECONDS;
+                       m.put("s", u); //$NON-NLS-1$
+                       m.put("sec", u); //$NON-NLS-1$
+                       m.put("secs", u); //$NON-NLS-1$
+                       m.put("second", u); //$NON-NLS-1$
+                       m.put("seconds", u); //$NON-NLS-1$
+
+                       u = MINUTES;
+                       m.put("m", u); //$NON-NLS-1$
+                       m.put("min", u); //$NON-NLS-1$
+                       m.put("mins", u); //$NON-NLS-1$
+                       m.put("minute", u); //$NON-NLS-1$
+                       m.put("minutes", u); //$NON-NLS-1$
+
+                       u = HOURS;
+                       m.put("h", u); //$NON-NLS-1$
+                       m.put("hr", u); //$NON-NLS-1$
+                       m.put("hrs", u); //$NON-NLS-1$
+                       m.put("hour", u); //$NON-NLS-1$
+                       m.put("hours", u); //$NON-NLS-1$
+
+                       u = DAYS;
+                       m.put("d", u); //$NON-NLS-1$
+                       m.put("day", u); //$NON-NLS-1$
+                       m.put("days", u); //$NON-NLS-1$
+
+                       UNITS = Collections.unmodifiableMap(m);
+               }
+
+               private UnitMap() {
+               }
+       }
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ReplicaFetchRequest.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ReplicaFetchRequest.java
new file mode 100644 (file)
index 0000000..201d9e9
--- /dev/null
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2016, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *   copyright notice, this list of conditions and the following
+ *   disclaimer in the documentation and/or other materials provided
+ *   with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *   names of its contributors may be used to endorse or promote
+ *   products derived from this software without specific prior
+ *   written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.internal.ketch;
+
+import java.util.Map;
+import java.util.Set;
+
+import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+
+/** A fetch request to obtain objects from a replica, and its result. */
+public class ReplicaFetchRequest {
+       private final Set<String> wantRefs;
+       private final Set<ObjectId> wantObjects;
+       private Map<String, Ref> refs;
+
+       /**
+        * Construct a new fetch request for a replica.
+        *
+        * @param wantRefs
+        *            named references to be fetched.
+        * @param wantObjects
+        *            specific objects to be fetched.
+        */
+       public ReplicaFetchRequest(Set<String> wantRefs,
+                       Set<ObjectId> wantObjects) {
+               this.wantRefs = wantRefs;
+               this.wantObjects = wantObjects;
+       }
+
+       /** @return references to be fetched. */
+       public Set<String> getWantRefs() {
+               return wantRefs;
+       }
+
+       /** @return objects to be fetched. */
+       public Set<ObjectId> getWantObjects() {
+               return wantObjects;
+       }
+
+       /** @return remote references, usually from the advertisement. */
+       @Nullable
+       public Map<String, Ref> getRefs() {
+               return refs;
+       }
+
+       /**
+        * @param refs
+        *            references observed from the replica.
+        */
+       public void setRefs(Map<String, Ref> refs) {
+               this.refs = refs;
+       }
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ReplicaPushRequest.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ReplicaPushRequest.java
new file mode 100644 (file)
index 0000000..691b142
--- /dev/null
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2016, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *   copyright notice, this list of conditions and the following
+ *   disclaimer in the documentation and/or other materials provided
+ *   with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *   names of its contributors may be used to endorse or promote
+ *   products derived from this software without specific prior
+ *   written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.internal.ketch;
+
+import java.util.Collection;
+import java.util.Map;
+
+import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.transport.ReceiveCommand;
+
+/**
+ * A push request sending objects to a replica, and its result.
+ * <p>
+ * Implementors of {@link KetchReplica} must populate the command result fields,
+ * {@link #setRefs(Map)}, and call one of
+ * {@link #setException(Repository, Throwable)} or {@link #done(Repository)} to
+ * finish processing.
+ */
+public class ReplicaPushRequest {
+       private final KetchReplica replica;
+       private final Collection<ReceiveCommand> commands;
+       private Map<String, Ref> refs;
+       private Throwable exception;
+       private boolean notified;
+
+       /**
+        * Construct a new push request for a replica.
+        *
+        * @param replica
+        *            the replica being pushed to.
+        * @param commands
+        *            commands to be executed.
+        */
+       public ReplicaPushRequest(KetchReplica replica,
+                       Collection<ReceiveCommand> commands) {
+               this.replica = replica;
+               this.commands = commands;
+       }
+
+       /** @return commands to be executed, and their results. */
+       public Collection<ReceiveCommand> getCommands() {
+               return commands;
+       }
+
+       /** @return remote references, usually from the advertisement. */
+       @Nullable
+       public Map<String, Ref> getRefs() {
+               return refs;
+       }
+
+       /**
+        * @param refs
+        *            references observed from the replica.
+        */
+       public void setRefs(Map<String, Ref> refs) {
+               this.refs = refs;
+       }
+
+       /** @return exception thrown, if any. */
+       @Nullable
+       public Throwable getException() {
+               return exception;
+       }
+
+       /**
+        * Mark the request as crashing with a communication error.
+        * <p>
+        * This method may take significant time acquiring the leader lock and
+        * updating the Ketch state machine with the failure.
+        *
+        * @param repo
+        *            local repository reference used by the push attempt.
+        * @param err
+        *            exception thrown during communication.
+        */
+       public void setException(@Nullable Repository repo, Throwable err) {
+               if (KetchReplica.log.isErrorEnabled()) {
+                       KetchReplica.log.error(describe("failed"), err); //$NON-NLS-1$
+               }
+               if (!notified) {
+                       notified = true;
+                       exception = err;
+                       replica.afterPush(repo, this);
+               }
+       }
+
+       /**
+        * Mark the request as completed without exception.
+        * <p>
+        * This method may take significant time acquiring the leader lock and
+        * updating the Ketch state machine with results from this replica.
+        *
+        * @param repo
+        *            local repository reference used by the push attempt.
+        */
+       public void done(Repository repo) {
+               if (KetchReplica.log.isDebugEnabled()) {
+                       KetchReplica.log.debug(describe("completed")); //$NON-NLS-1$
+               }
+               if (!notified) {
+                       notified = true;
+                       replica.afterPush(repo, this);
+               }
+       }
+
+       private String describe(String heading) {
+               StringBuilder b = new StringBuilder();
+               b.append("push to "); //$NON-NLS-1$
+               b.append(replica.describeForLog());
+               b.append(' ').append(heading).append(":\n"); //$NON-NLS-1$
+               for (ReceiveCommand cmd : commands) {
+                       b.append(String.format(
+                                       "  %-12s %-12s %s %s", //$NON-NLS-1$
+                                       LeaderSnapshot.str(cmd.getOldId()),
+                                       LeaderSnapshot.str(cmd.getNewId()),
+                                       cmd.getRefName(),
+                                       cmd.getResult()));
+                       if (cmd.getMessage() != null) {
+                               b.append(' ').append(cmd.getMessage());
+                       }
+                       b.append('\n');
+               }
+               return b.toString();
+       }
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ReplicaSnapshot.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ReplicaSnapshot.java
new file mode 100644 (file)
index 0000000..8c3de02
--- /dev/null
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2016, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *   copyright notice, this list of conditions and the following
+ *   disclaimer in the documentation and/or other materials provided
+ *   with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *   names of its contributors may be used to endorse or promote
+ *   products derived from this software without specific prior
+ *   written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.internal.ketch;
+
+import java.util.Date;
+
+import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.lib.ObjectId;
+
+/**
+ * A snapshot of a replica.
+ *
+ * @see LeaderSnapshot
+ */
+public class ReplicaSnapshot {
+       final KetchReplica replica;
+       ObjectId accepted;
+       ObjectId committed;
+       KetchReplica.State state;
+       String error;
+       long retryAtMillis;
+
+       ReplicaSnapshot(KetchReplica replica) {
+               this.replica = replica;
+       }
+
+       /** @return the replica this snapshot describes the state of. */
+       public KetchReplica getReplica() {
+               return replica;
+       }
+
+       /** @return current state of the replica. */
+       public KetchReplica.State getState() {
+               return state;
+       }
+
+       /** @return last known Git commit at {@code refs/txn/accepted}. */
+       @Nullable
+       public ObjectId getAccepted() {
+               return accepted;
+       }
+
+       /** @return last known Git commit at {@code refs/txn/committed}. */
+       @Nullable
+       public ObjectId getCommitted() {
+               return committed;
+       }
+
+       /**
+        * @return if {@link #getState()} == {@link KetchReplica.State#OFFLINE} an
+        *         optional human-readable message from the transport system
+        *         explaining the failure.
+        */
+       @Nullable
+       public String getErrorMessage() {
+               return error;
+       }
+
+       /**
+        * @return time (usually in the future) when the leader will retry
+        *         communication with the offline or lagging replica; null if no
+        *         retry is scheduled or necessary.
+        */
+       @Nullable
+       public Date getRetryAt() {
+               return retryAtMillis > 0 ? new Date(retryAtMillis) : null;
+       }
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/Round.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/Round.java
new file mode 100644 (file)
index 0000000..1335b85
--- /dev/null
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2016, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *   copyright notice, this list of conditions and the following
+ *   disclaimer in the documentation and/or other materials provided
+ *   with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *   names of its contributors may be used to endorse or promote
+ *   products derived from this software without specific prior
+ *   written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.internal.ketch;
+
+import java.io.IOException;
+import java.util.List;
+
+import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.transport.ReceiveCommand;
+
+/**
+ * One round-trip to all replicas proposing a log entry.
+ * <p>
+ * In Raft a log entry represents a state transition at a specific index in the
+ * replicated log. The leader can only append log entries to the log.
+ * <p>
+ * In Ketch a log entry is recorded under the {@code refs/txn} namespace. This
+ * occurs when:
+ * <ul>
+ * <li>a replica wants to establish itself as a new leader by proposing a new
+ * term (see {@link ElectionRound})
+ * <li>an established leader wants to gain consensus on new {@link Proposal}s
+ * (see {@link ProposalRound})
+ * </ul>
+ */
+abstract class Round {
+       final KetchLeader leader;
+       final LogIndex acceptedOldIndex;
+       LogIndex acceptedNewIndex;
+       List<ReceiveCommand> stageCommands;
+
+       Round(KetchLeader leader, LogIndex head) {
+               this.leader = leader;
+               this.acceptedOldIndex = head;
+       }
+
+       /**
+        * Creates a commit for {@code refs/txn/accepted} and calls
+        * {@link #runAsync(AnyObjectId)} to begin execution of the round across
+        * the system.
+        * <p>
+        * If references are being updated (such as in a {@link ProposalRound}) the
+        * RefTree may be modified.
+        * <p>
+        * Invoked without {@link KetchLeader#lock} to build objects.
+        *
+        * @throws IOException
+        *             the round cannot build new objects within the leader's
+        *             repository. The leader may be unable to execute.
+        */
+       abstract void start() throws IOException;
+
+       /**
+        * Asynchronously distribute the round's new value for
+        * {@code refs/txn/accepted} to all replicas.
+        * <p>
+        * Invoked by {@link #start()} after new commits have been created for the
+        * log. The method passes {@code newId} to {@link KetchLeader} to be
+        * distributed to all known replicas.
+        *
+        * @param newId
+        *            new value for {@code refs/txn/accepted}.
+        */
+       void runAsync(AnyObjectId newId) {
+               acceptedNewIndex = acceptedOldIndex.nextIndex(newId);
+               leader.runAsync(this);
+       }
+
+       /**
+        * Notify the round it was accepted by a majority of the system.
+        * <p>
+        * Invoked by the leader with {@link KetchLeader#lock} held by the caller.
+        */
+       abstract void success();
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/StageBuilder.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/StageBuilder.java
new file mode 100644 (file)
index 0000000..61871a4
--- /dev/null
@@ -0,0 +1,270 @@
+/*
+ * Copyright (C) 2016, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *   copyright notice, this list of conditions and the following
+ *   disclaimer in the documentation and/or other materials provided
+ *   with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *   names of its contributors may be used to endorse or promote
+ *   products derived from this software without specific prior
+ *   written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.internal.ketch;
+
+import static org.eclipse.jgit.lib.FileMode.TYPE_GITLINK;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevObject;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.ReceiveCommand;
+import org.eclipse.jgit.treewalk.EmptyTreeIterator;
+import org.eclipse.jgit.treewalk.TreeWalk;
+import org.eclipse.jgit.treewalk.filter.TreeFilter;
+
+/** Constructs a set of commands to stage content during a proposal. */
+public class StageBuilder {
+       /**
+        * Acceptable number of references to send in a single stage transaction.
+        * <p>
+        * If the number of unique objects exceeds this amount the builder will
+        * attempt to decrease the reference count by chaining commits..
+        */
+       private static final int SMALL_BATCH_SIZE = 5;
+
+       /**
+        * Acceptable number of commits to chain together using parent pointers.
+        * <p>
+        * When staging many unique commits the {@link StageBuilder} batches
+        * together unrelated commits as parents of a temporary commit. After the
+        * proposal completes the temporary commit is discarded and can be garbage
+        * collected by all replicas.
+        */
+       private static final int TEMP_PARENT_BATCH_SIZE = 128;
+
+       private static final byte[] PEEL = { ' ', '^' };
+
+       private final String txnStage;
+       private final String txnId;
+
+       /**
+        * Construct a stage builder for a transaction.
+        *
+        * @param txnStageNamespace
+        *            namespace for transaction references to build
+        *            {@code "txnStageNamespace/txnId.n"} style names.
+        * @param txnId
+        *            identifier used to name temporary staging refs.
+        */
+       public StageBuilder(String txnStageNamespace, ObjectId txnId) {
+               this.txnStage = txnStageNamespace;
+               this.txnId = txnId.name();
+       }
+
+       /**
+        * Compare two RefTrees and return commands to stage new objects.
+        * <p>
+        * This method ignores the lineage between the two RefTrees and does a
+        * straight diff on the two trees. New objects will be staged. The diff
+        * strategy is useful to catch-up a lagging replica, without sending every
+        * intermediate step. This may mean the replica does not have the same
+        * object set as other replicas if there are rewinds or branch deletes.
+        *
+        * @param git
+        *            source repository to read {@code oldTree} and {@code newTree}
+        *            from.
+        * @param oldTree
+        *            accepted RefTree on the replica ({@code refs/txn/accepted}).
+        *            Use {@link ObjectId#zeroId()} if the remote does not have any
+        *            ref tree, e.g. a new replica catching up.
+        * @param newTree
+        *            RefTree being sent to the replica. The trees will be compared.
+        * @return list of commands to create {@code "refs/txn/stage/..."}
+        *         references on replicas anchoring new objects into the repository
+        *         while a transaction gains consensus.
+        * @throws IOException
+        *             {@code git} cannot be accessed to compare {@code oldTree} and
+        *             {@code newTree} to build the object set.
+        */
+       public List<ReceiveCommand> makeStageList(Repository git, ObjectId oldTree,
+                       ObjectId newTree) throws IOException {
+               try (RevWalk rw = new RevWalk(git);
+                               TreeWalk tw = new TreeWalk(rw.getObjectReader());
+                               ObjectInserter ins = git.newObjectInserter()) {
+                       if (AnyObjectId.equals(oldTree, ObjectId.zeroId())) {
+                               tw.addTree(new EmptyTreeIterator());
+                       } else {
+                               tw.addTree(rw.parseTree(oldTree));
+                       }
+                       tw.addTree(rw.parseTree(newTree));
+                       tw.setFilter(TreeFilter.ANY_DIFF);
+                       tw.setRecursive(true);
+
+                       Set<ObjectId> newObjs = new HashSet<>();
+                       while (tw.next()) {
+                               if (tw.getRawMode(1) == TYPE_GITLINK
+                                               && !tw.isPathSuffix(PEEL, 2)) {
+                                       newObjs.add(tw.getObjectId(1));
+                               }
+                       }
+
+                       List<ReceiveCommand> cmds = makeStageList(newObjs, git, ins);
+                       ins.flush();
+                       return cmds;
+               }
+       }
+
+       /**
+        * Construct a set of commands to stage objects on a replica.
+        *
+        * @param newObjs
+        *            objects to send to a replica.
+        * @param git
+        *            local repository to read source objects from. Required to
+        *            perform minification of {@code newObjs}.
+        * @param inserter
+        *            inserter to write temporary commit objects during minification
+        *            if many new branches are created by {@code newObjs}.
+        * @return list of commands to create {@code "refs/txn/stage/..."}
+        *         references on replicas anchoring {@code newObjs} into the
+        *         repository while a transaction gains consensus.
+        * @throws IOException
+        *             {@code git} cannot be accessed to perform minification of
+        *             {@code newObjs}.
+        */
+       public List<ReceiveCommand> makeStageList(Set<ObjectId> newObjs,
+                       @Nullable Repository git, @Nullable ObjectInserter inserter)
+                                       throws IOException {
+               if (git == null || newObjs.size() <= SMALL_BATCH_SIZE) {
+                       // Without a source repository can only construct unique set.
+                       List<ReceiveCommand> cmds = new ArrayList<>(newObjs.size());
+                       for (ObjectId id : newObjs) {
+                               stage(cmds, id);
+                       }
+                       return cmds;
+               }
+
+               List<ReceiveCommand> cmds = new ArrayList<>();
+               List<RevCommit> commits = new ArrayList<>();
+               reduceObjects(cmds, commits, git, newObjs);
+
+               if (inserter == null || commits.size() <= 1
+                               || (cmds.size() + commits.size()) <= SMALL_BATCH_SIZE) {
+                       // Without an inserter to aggregate commits, or for a small set of
+                       // commits just send one stage ref per commit.
+                       for (RevCommit c : commits) {
+                               stage(cmds, c.copy());
+                       }
+                       return cmds;
+               }
+
+               // 'commits' is sorted most recent to least recent commit.
+               // Group batches of commits and build a chain.
+               // TODO(sop) Cluster by restricted graphs to support filtering.
+               ObjectId tip = null;
+               for (int end = commits.size(); end > 0;) {
+                       int start = Math.max(0, end - TEMP_PARENT_BATCH_SIZE);
+                       List<RevCommit> batch = commits.subList(start, end);
+                       List<ObjectId> parents = new ArrayList<>(1 + batch.size());
+                       if (tip != null) {
+                               parents.add(tip);
+                       }
+                       parents.addAll(batch);
+
+                       CommitBuilder b = new CommitBuilder();
+                       b.setTreeId(batch.get(0).getTree());
+                       b.setParentIds(parents);
+                       b.setAuthor(tmpAuthor(batch));
+                       b.setCommitter(b.getAuthor());
+                       tip = inserter.insert(b);
+                       end = start;
+               }
+               stage(cmds, tip);
+               return cmds;
+       }
+
+       private static PersonIdent tmpAuthor(List<RevCommit> commits) {
+               // Construct a predictable author using most recent commit time.
+               int t = 0;
+               for (int i = 0; i < commits.size();) {
+                       t = Math.max(t, commits.get(i).getCommitTime());
+               }
+               String name = "Ketch Stage"; //$NON-NLS-1$
+               String email = "tmp@tmp"; //$NON-NLS-1$
+               return new PersonIdent(name, email, t * 1000L, 0);
+       }
+
+       private void reduceObjects(List<ReceiveCommand> cmds,
+                       List<RevCommit> commits, Repository git,
+                       Set<ObjectId> newObjs) throws IOException {
+               try (RevWalk rw = new RevWalk(git)) {
+                       rw.setRetainBody(false);
+
+                       for (ObjectId id : newObjs) {
+                               RevObject obj = rw.parseAny(id);
+                               if (obj instanceof RevCommit) {
+                                       rw.markStart((RevCommit) obj);
+                               } else {
+                                       stage(cmds, id);
+                               }
+                       }
+
+                       for (RevCommit c; (c = rw.next()) != null;) {
+                               commits.add(c);
+                               rw.markUninteresting(c);
+                       }
+               }
+       }
+
+       private void stage(List<ReceiveCommand> cmds, ObjectId id) {
+               int estLen = txnStage.length() + txnId.length() + 5;
+               StringBuilder n = new StringBuilder(estLen);
+               n.append(txnStage).append(txnId).append('.');
+               n.append(Integer.toHexString(cmds.size()));
+               cmds.add(new ReceiveCommand(ObjectId.zeroId(), id, n.toString()));
+       }
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/package-info.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/package-info.java
new file mode 100644 (file)
index 0000000..dfe0375
--- /dev/null
@@ -0,0 +1,4 @@
+/**
+ * Distributed consensus system built on Git.
+ */
+package org.eclipse.jgit.internal.ketch;