diff options
Diffstat (limited to 'org.eclipse.jgit/src')
42 files changed, 5076 insertions, 1166 deletions
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java index 5b7d84f732..b5057ad282 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java @@ -53,6 +53,7 @@ import java.util.List; import org.eclipse.jgit.api.errors.AbortedByHookException; import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException; +import org.eclipse.jgit.api.errors.EmtpyCommitException; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.api.errors.JGitInternalException; import org.eclipse.jgit.api.errors.NoFilepatternException; @@ -130,6 +131,8 @@ public class CommitCommand extends GitCommand<RevCommit> { private PrintStream hookOutRedirect; + private Boolean allowEmpty; + /** * @param repo */ @@ -179,7 +182,7 @@ public class CommitCommand extends GitCommand<RevCommit> { processOptions(state, rw); - if (all && !repo.isBare() && repo.getWorkTree() != null) { + if (all && !repo.isBare()) { try (Git git = new Git(repo)) { git.add() .addFilepattern(".") //$NON-NLS-1$ @@ -231,6 +234,16 @@ public class CommitCommand extends GitCommand<RevCommit> { if (insertChangeId) insertChangeId(indexTreeId); + // Check for empty commits + if (headId != null && !allowEmpty.booleanValue()) { + RevCommit headCommit = rw.parseCommit(headId); + headCommit.getTree(); + if (indexTreeId.equals(headCommit.getTree())) { + throw new EmtpyCommitException( + JGitText.get().emptyCommit); + } + } + // Create a Commit object, populate it and write it CommitBuilder commit = new CommitBuilder(); commit.setCommitter(committer); @@ -457,6 +470,8 @@ public class CommitCommand extends GitCommand<RevCommit> { // there must be at least one change if (emptyCommit) + // Would like to throw a EmptyCommitException. But this would break the API + // TODO(ch): Change this in the next release throw new JGitInternalException(JGitText.get().emptyCommit); // update index @@ -510,6 +525,12 @@ public class CommitCommand extends GitCommand<RevCommit> { committer = new PersonIdent(repo); if (author == null && !amend) author = committer; + if (allowEmpty == null) + // JGit allows empty commits by default. Only when pathes are + // specified the commit should not be empty. This behaviour differs + // from native git but can only be adapted in the next release. + // TODO(ch) align the defaults with native git + allowEmpty = (only.isEmpty()) ? Boolean.TRUE : Boolean.FALSE; // when doing a merge commit parse MERGE_HEAD and MERGE_MSG files if (state == RepositoryState.MERGING_RESOLVED @@ -579,6 +600,27 @@ public class CommitCommand extends GitCommand<RevCommit> { } /** + * @param allowEmpty + * whether it should be allowed to create a commit which has the + * same tree as it's sole predecessor (a commit which doesn't + * change anything). By default when creating standard commits + * (without specifying paths) JGit allows to create such commits. + * When this flag is set to false an attempt to create an "empty" + * standard commit will lead to an EmptyCommitException. + * <p> + * By default when creating a commit containing only specified + * paths an attempt to create an empty commit leads to a + * {@link JGitInternalException}. By setting this flag to + * <code>true</code> this exception will not be thrown. + * @return {@code this} + * @since 4.2 + */ + public CommitCommand setAllowEmpty(boolean allowEmpty) { + this.allowEmpty = Boolean.valueOf(allowEmpty); + return this; + } + + /** * @return the commit message used for the <code>commit</code> */ public String getMessage() { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/FetchCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/FetchCommand.java index 9620089b08..de512761a4 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/FetchCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/FetchCommand.java @@ -116,22 +116,17 @@ public class FetchCommand extends TransportCommand<FetchCommand, FetchResult> { org.eclipse.jgit.api.errors.TransportException { checkCallable(); - try { - Transport transport = Transport.open(repo, remote); - try { - transport.setCheckFetchedObjects(checkFetchedObjects); - transport.setRemoveDeletedRefs(isRemoveDeletedRefs()); - transport.setDryRun(dryRun); - if (tagOption != null) - transport.setTagOpt(tagOption); - transport.setFetchThin(thin); - configure(transport); - - FetchResult result = transport.fetch(monitor, refSpecs); - return result; - } finally { - transport.close(); - } + try (Transport transport = Transport.open(repo, remote)) { + transport.setCheckFetchedObjects(checkFetchedObjects); + transport.setRemoveDeletedRefs(isRemoveDeletedRefs()); + transport.setDryRun(dryRun); + if (tagOption != null) + transport.setTagOpt(tagOption); + transport.setFetchThin(thin); + configure(transport); + + FetchResult result = transport.fetch(monitor, refSpecs); + return result; } catch (NoRemoteRepositoryException e) { throw new InvalidRemoteException(MessageFormat.format( JGitText.get().invalidRemote, remote), e); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/LsRemoteCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/LsRemoteCommand.java index 3363a0fc8f..f3527fd805 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/LsRemoteCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/LsRemoteCommand.java @@ -182,13 +182,9 @@ public class LsRemoteCommand extends org.eclipse.jgit.api.errors.TransportException { checkCallable(); - Transport transport = null; - FetchConnection fc = null; - try { - if (repo != null) - transport = Transport.open(repo, remote); - else - transport = Transport.open(new URIish(remote)); + try (Transport transport = repo != null + ? Transport.open(repo, remote) + : Transport.open(new URIish(remote))) { transport.setOptionUploadPack(uploadPack); configure(transport); Collection<RefSpec> refSpecs = new ArrayList<RefSpec>(1); @@ -199,19 +195,20 @@ public class LsRemoteCommand extends refSpecs.add(new RefSpec("refs/heads/*:refs/remotes/origin/*")); //$NON-NLS-1$ Collection<Ref> refs; Map<String, Ref> refmap = new HashMap<String, Ref>(); - fc = transport.openFetch(); - refs = fc.getRefs(); - if (refSpecs.isEmpty()) - for (Ref r : refs) - refmap.put(r.getName(), r); - else - for (Ref r : refs) - for (RefSpec rs : refSpecs) - if (rs.matchSource(r)) { - refmap.put(r.getName(), r); - break; - } - return refmap; + try (FetchConnection fc = transport.openFetch()) { + refs = fc.getRefs(); + if (refSpecs.isEmpty()) + for (Ref r : refs) + refmap.put(r.getName(), r); + else + for (Ref r : refs) + for (RefSpec rs : refSpecs) + if (rs.matchSource(r)) { + refmap.put(r.getName(), r); + break; + } + return refmap; + } } catch (URISyntaxException e) { throw new InvalidRemoteException(MessageFormat.format( JGitText.get().invalidRemote, remote)); @@ -223,11 +220,6 @@ public class LsRemoteCommand extends throw new org.eclipse.jgit.api.errors.TransportException( e.getMessage(), e); - } finally { - if (fc != null) - fc.close(); - if (transport != null) - transport.close(); } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/ResetCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/ResetCommand.java index 8f4bc4f26c..4c91e6c17f 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/ResetCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/ResetCommand.java @@ -190,10 +190,8 @@ public class ResetCommand extends GitCommand<Ref> { ObjectId origHead = ru.getOldObjectId(); if (origHead != null) repo.writeOrigHead(origHead); - result = ru.getRef(); - } else { - result = repo.getRef(Constants.HEAD); } + result = repo.exactRef(Constants.HEAD); if (mode == null) mode = ResetType.MIXED; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/errors/EmtpyCommitException.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/errors/EmtpyCommitException.java new file mode 100644 index 0000000000..b3cc1bfcf2 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/errors/EmtpyCommitException.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2015, Christian Halstrick <christian.halstrick@sap.com> + * 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.api.errors; + +/** + * Exception thrown when a newly created commit does not contain any changes + * + * @since 4.2 + */ +public class EmtpyCommitException extends GitAPIException { + private static final long serialVersionUID = 1L; + + /** + * @param message + * @param cause + */ + public EmtpyCommitException(String message, Throwable cause) { + super(message, cause); + } + + /** + * @param message + */ + public EmtpyCommitException(String message) { + super(message); + } +} 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 index 0000000000..014eab2b45 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ElectionRound.java @@ -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 index 0000000000..171c059db1 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchConstants.java @@ -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 index 0000000000..3bcd6bcb24 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchLeader.java @@ -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 (see {@link KetchPreReceive} for an example): + * <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/KetchLeaderCache.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchLeaderCache.java new file mode 100644 index 0000000000..ba033c1a42 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchLeaderCache.java @@ -0,0 +1,126 @@ +/* + * 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.net.URISyntaxException; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import org.eclipse.jgit.internal.storage.dfs.DfsRepository; +import org.eclipse.jgit.lib.Repository; + +/** + * A cache of live leader instances, keyed by repository. + * <p> + * Ketch only assigns a leader to a repository when needed. If + * {@link #get(Repository)} is called for a repository that does not have a + * leader, the leader is created and added to the cache. + */ +public class KetchLeaderCache { + private final KetchSystem system; + private final ConcurrentMap<String, KetchLeader> leaders; + private final Lock startLock; + + /** + * Initialize a new leader cache. + * + * @param system + * system configuration for the leaders + */ + public KetchLeaderCache(KetchSystem system) { + this.system = system; + leaders = new ConcurrentHashMap<>(); + startLock = new ReentrantLock(true /* fair */); + } + + /** + * Lookup the leader instance for a given repository. + * + * @param repo + * repository to get the leader for. + * @return the leader instance for the repository. + * @throws URISyntaxException + * remote configuration contains an invalid URL. + */ + public KetchLeader get(Repository repo) + throws URISyntaxException { + String key = computeKey(repo); + KetchLeader leader = leaders.get(key); + if (leader != null) { + return leader; + } + return startLeader(key, repo); + } + + private KetchLeader startLeader(String key, Repository repo) + throws URISyntaxException { + startLock.lock(); + try { + KetchLeader leader = leaders.get(key); + if (leader != null) { + return leader; + } + leader = system.createLeader(repo); + leaders.put(key, leader); + return leader; + } finally { + startLock.unlock(); + } + } + + private static String computeKey(Repository repo) { + if (repo instanceof DfsRepository) { + DfsRepository dfs = (DfsRepository) repo; + return dfs.getDescription().getRepositoryName(); + } + + if (repo.getDirectory() != null) { + return repo.getDirectory().toURI().toString(); + } + + throw new IllegalArgumentException(); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchPreReceive.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchPreReceive.java new file mode 100644 index 0000000000..1b4307f3fb --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchPreReceive.java @@ -0,0 +1,155 @@ +/* + * 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 java.util.concurrent.TimeUnit.SECONDS; +import static org.eclipse.jgit.internal.ketch.Proposal.State.EXECUTED; +import static org.eclipse.jgit.internal.ketch.Proposal.State.QUEUED; +import static org.eclipse.jgit.transport.ReceiveCommand.Result.NOT_ATTEMPTED; +import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_OTHER_REASON; + +import java.io.IOException; +import java.util.Collection; + +import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.transport.PreReceiveHook; +import org.eclipse.jgit.transport.ProgressSpinner; +import org.eclipse.jgit.transport.ReceiveCommand; +import org.eclipse.jgit.transport.ReceivePack; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * PreReceiveHook for handling push traffic in a Ketch system. + * <p> + * Install an instance on {@link ReceivePack} to capture the commands and other + * connection state and relay them through the {@link KetchLeader}, allowing the + * leader to gain consensus about the new reference state. + */ +public class KetchPreReceive implements PreReceiveHook { + private static final Logger log = LoggerFactory.getLogger(KetchPreReceive.class); + + private final KetchLeader leader; + + /** + * Construct a hook executing updates through a {@link KetchLeader}. + * + * @param leader + * leader for this repository. + */ + public KetchPreReceive(KetchLeader leader) { + this.leader = leader; + } + + @Override + public void onPreReceive(ReceivePack rp, Collection<ReceiveCommand> cmds) { + cmds = ReceiveCommand.filter(cmds, NOT_ATTEMPTED); + if (cmds.isEmpty()) { + return; + } + + try { + Proposal proposal = new Proposal(rp.getRevWalk(), cmds) + .setPushCertificate(rp.getPushCertificate()) + .setAuthor(rp.getRefLogIdent()) + .setMessage("push"); //$NON-NLS-1$ + leader.queueProposal(proposal); + if (proposal.isDone()) { + // This failed fast, e.g. conflict or bad precondition. + return; + } + + ProgressSpinner spinner = new ProgressSpinner( + rp.getMessageOutputStream()); + if (proposal.getState() == QUEUED) { + waitForQueue(proposal, spinner); + } + if (!proposal.isDone()) { + waitForPropose(proposal, spinner); + } + } catch (IOException | InterruptedException e) { + String msg = JGitText.get().transactionAborted; + for (ReceiveCommand cmd : cmds) { + if (cmd.getResult() == NOT_ATTEMPTED) { + cmd.setResult(REJECTED_OTHER_REASON, msg); + } + } + log.error(msg, e); + } + } + + private void waitForQueue(Proposal proposal, ProgressSpinner spinner) + throws InterruptedException { + spinner.beginTask(KetchText.get().waitingForQueue, 1, SECONDS); + while (!proposal.awaitStateChange(QUEUED, 250, MILLISECONDS)) { + spinner.update(); + } + switch (proposal.getState()) { + case RUNNING: + default: + spinner.endTask(KetchText.get().starting); + break; + + case EXECUTED: + spinner.endTask(KetchText.get().accepted); + break; + + case ABORTED: + spinner.endTask(KetchText.get().failed); + break; + } + } + + private void waitForPropose(Proposal proposal, ProgressSpinner spinner) + throws InterruptedException { + spinner.beginTask(KetchText.get().proposingUpdates, 2, SECONDS); + while (!proposal.await(250, MILLISECONDS)) { + spinner.update(); + } + spinner.endTask(proposal.getState() == EXECUTED + ? KetchText.get().accepted + : KetchText.get().failed); + } +} 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 index 0000000000..a30bbb260a --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchReplica.java @@ -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 index 0000000000..71e872e3fa --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchSystem.java @@ -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/lib/SymlinkTreeEntry.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchText.java index c7e41bce04..b6c3bc92c5 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/SymlinkTreeEntry.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchText.java @@ -1,6 +1,5 @@ /* - * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com> - * Copyright (C) 2006-2007, Shawn O. Pearce <spearce@spearce.org> + * 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 @@ -42,44 +41,30 @@ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package org.eclipse.jgit.lib; +package org.eclipse.jgit.internal.ketch; -/** - * A tree entry representing a symbolic link. - * - * Note. Java cannot really handle these as file system objects. - * - * @deprecated To look up information about a single path, use - * {@link org.eclipse.jgit.treewalk.TreeWalk#forPath(Repository, String, org.eclipse.jgit.revwalk.RevTree)}. - * To lookup information about multiple paths at once, use a - * {@link org.eclipse.jgit.treewalk.TreeWalk} and obtain the current entry's - * information from its getter methods. - */ -@Deprecated -public class SymlinkTreeEntry extends TreeEntry { +import org.eclipse.jgit.nls.NLS; +import org.eclipse.jgit.nls.TranslationBundle; - /** - * Construct a {@link SymlinkTreeEntry} with the specified name and SHA-1 in - * the specified parent - * - * @param parent - * @param id - * @param nameUTF8 - */ - public SymlinkTreeEntry(final Tree parent, final ObjectId id, - final byte[] nameUTF8) { - super(parent, id, nameUTF8); +/** 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); } - public FileMode getMode() { - return FileMode.SYMLINK; - } - - public String toString() { - final StringBuilder r = new StringBuilder(); - r.append(ObjectId.toString(getId())); - r.append(" S "); //$NON-NLS-1$ - r.append(getFullName()); - return r.toString(); - } + // @formatter:off + /***/ public String accepted; + /***/ public String cannotFetchFromLocalReplica; + /***/ public String failed; + /***/ public String invalidFollowerUri; + /***/ public String leaderFailedToStore; + /***/ public String localReplicaRequired; + /***/ public String mismatchedTxnNamespace; + /***/ public String outsideTxnNamespace; + /***/ public String proposingUpdates; + /***/ public String queuedProposalFailedToApply; + /***/ public String starting; + /***/ public String unsupportedVoterCount; + /***/ public String waitingForQueue; } 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 index 0000000000..35327ea0b3 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/LagCheck.java @@ -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 index 0000000000..28a49df97a --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/LeaderSnapshot.java @@ -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 index 0000000000..e297bca45e --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/LocalReplica.java @@ -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 index 0000000000..350c8ed62e --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/LogIndex.java @@ -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 index 0000000000..0876eb5dbd --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/Proposal.java @@ -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 index 0000000000..d34477ab26 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ProposalRound.java @@ -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 index 0000000000..6f4a178673 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/RemoteGitReplica.java @@ -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 index 0000000000..e16e63aa7e --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ReplicaConfig.java @@ -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/lib/GitlinkTreeEntry.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ReplicaFetchRequest.java index 936fd82bfd..201d9e9743 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/GitlinkTreeEntry.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ReplicaFetchRequest.java @@ -1,7 +1,5 @@ /* - * Copyright (C) 2009, Jonas Fonseca <fonseca@diku.dk> - * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com> - * Copyright (C) 2007, Shawn O. Pearce <spearce@spearce.org> + * 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 @@ -43,45 +41,56 @@ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package org.eclipse.jgit.lib; +package org.eclipse.jgit.internal.ketch; -/** - * A tree entry representing a gitlink entry used for submodules. - * - * Note. Java cannot really handle these as file system objects. - * - * @deprecated To look up information about a single path, use - * {@link org.eclipse.jgit.treewalk.TreeWalk#forPath(Repository, String, org.eclipse.jgit.revwalk.RevTree)}. - * To lookup information about multiple paths at once, use a - * {@link org.eclipse.jgit.treewalk.TreeWalk} and obtain the current entry's - * information from its getter methods. - */ -@Deprecated -public class GitlinkTreeEntry extends TreeEntry { +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 {@link GitlinkTreeEntry} with the specified name and SHA-1 in - * the specified parent + * Construct a new fetch request for a replica. * - * @param parent - * @param id - * @param nameUTF8 + * @param wantRefs + * named references to be fetched. + * @param wantObjects + * specific objects to be fetched. */ - public GitlinkTreeEntry(final Tree parent, final ObjectId id, - final byte[] nameUTF8) { - super(parent, id, nameUTF8); + public ReplicaFetchRequest(Set<String> wantRefs, + Set<ObjectId> wantObjects) { + this.wantRefs = wantRefs; + this.wantObjects = wantObjects; } - public FileMode getMode() { - return FileMode.GITLINK; + /** @return references to be fetched. */ + public Set<String> getWantRefs() { + return wantRefs; } - @Override - public String toString() { - final StringBuilder r = new StringBuilder(); - r.append(ObjectId.toString(getId())); - r.append(" G "); //$NON-NLS-1$ - r.append(getFullName()); - return r.toString(); + /** @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 index 0000000000..691b1424f4 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ReplicaPushRequest.java @@ -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 index 0000000000..8c3de027d2 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ReplicaSnapshot.java @@ -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 index 0000000000..1335b85cca --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/Round.java @@ -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 index 0000000000..61871a494f --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/StageBuilder.java @@ -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 index 0000000000..dfe03752ca --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/package-info.java @@ -0,0 +1,4 @@ +/** + * Distributed consensus system built on Git. + */ +package org.eclipse.jgit.internal.ketch; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/InMemoryRepository.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/InMemoryRepository.java index a050e1a5bd..5e246b47b4 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/InMemoryRepository.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/InMemoryRepository.java @@ -16,7 +16,6 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; -import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.internal.storage.pack.PackExt; import org.eclipse.jgit.lib.BatchRefUpdate; import org.eclipse.jgit.lib.ObjectId; @@ -312,7 +311,7 @@ public class InMemoryRepository extends DfsRepository { try (RevWalk rw = new RevWalk(getRepository())) { for (ReceiveCommand c : cmds) { if (c.getResult() != ReceiveCommand.Result.NOT_ATTEMPTED) { - reject(cmds); + ReceiveCommand.abort(cmds); return; } @@ -324,7 +323,7 @@ public class InMemoryRepository extends DfsRepository { } } catch (IOException e) { c.setResult(ReceiveCommand.Result.REJECTED_MISSING_OBJECT); - reject(cmds); + ReceiveCommand.abort(cmds); return; } } @@ -337,7 +336,7 @@ public class InMemoryRepository extends DfsRepository { if (r == null) { if (c.getType() != ReceiveCommand.Type.CREATE) { c.setResult(ReceiveCommand.Result.LOCK_FAILURE); - reject(cmds); + ReceiveCommand.abort(cmds); return; } } else { @@ -345,7 +344,7 @@ public class InMemoryRepository extends DfsRepository { if (r.isSymbolic() || objectId == null || !objectId.equals(c.getOldId())) { c.setResult(ReceiveCommand.Result.LOCK_FAILURE); - reject(cmds); + ReceiveCommand.abort(cmds); return; } } @@ -374,15 +373,6 @@ public class InMemoryRepository extends DfsRepository { clearCache(); } - private void reject(List<ReceiveCommand> cmds) { - for (ReceiveCommand c : cmds) { - if (c.getResult() == ReceiveCommand.Result.NOT_ATTEMPTED) { - c.setResult(ReceiveCommand.Result.REJECTED_OTHER_REASON, - JGitText.get().transactionAborted); - } - } - } - @Override protected boolean compareAndPut(Ref oldRef, Ref newRef) throws IOException { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/Command.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/Command.java index 540c4384a7..dd08375f21 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/Command.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/Command.java @@ -49,12 +49,14 @@ import static org.eclipse.jgit.lib.FileMode.TYPE_GITLINK; import static org.eclipse.jgit.lib.FileMode.TYPE_SYMLINK; import static org.eclipse.jgit.lib.Ref.Storage.NETWORK; import static org.eclipse.jgit.transport.ReceiveCommand.Result.NOT_ATTEMPTED; +import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_OTHER_REASON; import java.io.IOException; import org.eclipse.jgit.annotations.Nullable; import org.eclipse.jgit.dircache.DirCacheEntry; import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectIdRef; import org.eclipse.jgit.lib.ObjectInserter; @@ -79,6 +81,30 @@ import org.eclipse.jgit.transport.ReceiveCommand.Result; * for processing. */ public class Command { + /** + * Set unprocessed commands as failed due to transaction aborted. + * <p> + * If a command is still {@link Result#NOT_ATTEMPTED} it will be set to + * {@link Result#REJECTED_OTHER_REASON}. If {@code why} is non-null its + * contents will be used as the message for the first command status. + * + * @param commands + * commands to mark as failed. + * @param why + * optional message to set on the first aborted command. + */ + public static void abort(Iterable<Command> commands, @Nullable String why) { + if (why == null || why.isEmpty()) { + why = JGitText.get().transactionAborted; + } + for (Command c : commands) { + if (c.getResult() == NOT_ATTEMPTED) { + c.setResult(REJECTED_OTHER_REASON, why); + why = JGitText.get().transactionAborted; + } + } + } + private final Ref oldRef; private final Ref newRef; private final ReceiveCommand cmd; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTree.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTree.java index 43eec5185b..85690c8ca5 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTree.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTree.java @@ -55,7 +55,6 @@ import static org.eclipse.jgit.lib.Ref.Storage.NEW; import static org.eclipse.jgit.lib.Ref.Storage.PACKED; import static org.eclipse.jgit.lib.RefDatabase.MAX_SYMBOLIC_REF_DEPTH; 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.REJECTED_OTHER_REASON; import java.io.IOException; @@ -248,7 +247,8 @@ public class RefTree { if (!isValidRef(cmd)) { cmd.setResult(REJECTED_OTHER_REASON, JGitText.get().funnyRefname); - return abort(cmdList); + Command.abort(cmdList, null); + return false; } apply(ed, cmd); } @@ -264,9 +264,11 @@ public class RefTree { break; } } - return abort(cmdList); + Command.abort(cmdList, null); + return false; } catch (LockFailureException e) { - return abort(cmdList); + Command.abort(cmdList, null); + return false; } } @@ -342,19 +344,6 @@ public class RefTree { } } - private static boolean abort(Iterable<Command> cmdList) { - for (Command cmd : cmdList) { - if (cmd.getResult() == NOT_ATTEMPTED) { - reject(cmd, JGitText.get().transactionAborted); - } - } - return false; - } - - private static void reject(Command cmd, String msg) { - cmd.setResult(REJECTED_OTHER_REASON, msg); - } - /** * Convert a path name in a RefTree to the reference name known by Git. * diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTreeBatch.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTreeBatch.java index 0cedea94d0..a55a9f51e7 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTreeBatch.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTreeBatch.java @@ -98,7 +98,7 @@ class RefTreeBatch extends BatchRefUpdate { } if (c.getType() == UPDATE_NONFASTFORWARD) { c.setResult(REJECTED_NONFASTFORWARD); - reject(); + ReceiveCommand.abort(getCommands()); return; } } @@ -108,15 +108,6 @@ class RefTreeBatch extends BatchRefUpdate { execute(rw, todo); } - private void reject() { - String aborted = JGitText.get().transactionAborted; - for (ReceiveCommand c : getCommands()) { - if (c.getResult() == NOT_ATTEMPTED) { - c.setResult(REJECTED_OTHER_REASON, aborted); - } - } - } - void init(RevWalk rw) throws IOException { src = refdb.getBootstrap().exactRef(refdb.getTxnCommitted()); if (src != null && src.getObjectId() != null) { @@ -150,13 +141,13 @@ class RefTreeBatch extends BatchRefUpdate { void execute(RevWalk rw, List<Command> todo) throws IOException { for (Command c : todo) { if (c.getResult() != NOT_ATTEMPTED) { - reject(todo, JGitText.get().transactionAborted); + Command.abort(todo, null); return; } if (refdb.conflictsWithBootstrap(c.getRefName())) { c.setResult(REJECTED_OTHER_REASON, MessageFormat .format(JGitText.get().invalidRefName, c.getRefName())); - reject(todo, JGitText.get().transactionAborted); + Command.abort(todo, null); return; } } @@ -210,7 +201,7 @@ class RefTreeBatch extends BatchRefUpdate { c.setResult(OK); } } else { - reject(todo, commit.getResult().name()); + Command.abort(todo, commit.getResult().name()); } } @@ -228,13 +219,4 @@ class RefTreeBatch extends BatchRefUpdate { u.addCommand(commit); u.execute(rw, NullProgressMonitor.INSTANCE); } - - private static void reject(List<Command> todo, String msg) { - for (Command c : todo) { - if (c.getResult() == NOT_ATTEMPTED) { - c.setResult(REJECTED_OTHER_REASON, msg); - msg = JGitText.get().transactionAborted; - } - } - } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/FileTreeEntry.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/FileTreeEntry.java deleted file mode 100644 index 6811417ee0..0000000000 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/FileTreeEntry.java +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com> - * Copyright (C) 2006-2007, Shawn O. Pearce <spearce@spearce.org> - * 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.lib; - -import java.io.IOException; - -/** - * A representation of a file (blob) object in a {@link Tree}. - * - * @deprecated To look up information about a single path, use - * {@link org.eclipse.jgit.treewalk.TreeWalk#forPath(Repository, String, org.eclipse.jgit.revwalk.RevTree)}. - * To lookup information about multiple paths at once, use a - * {@link org.eclipse.jgit.treewalk.TreeWalk} and obtain the current entry's - * information from its getter methods. - */ -@Deprecated -public class FileTreeEntry extends TreeEntry { - private FileMode mode; - - /** - * Constructor for a File (blob) object. - * - * @param parent - * The {@link Tree} holding this object (or null) - * @param id - * the SHA-1 of the blob (or null for a yet unhashed file) - * @param nameUTF8 - * raw object name in the parent tree - * @param execute - * true if the executable flag is set - */ - public FileTreeEntry(final Tree parent, final ObjectId id, - final byte[] nameUTF8, final boolean execute) { - super(parent, id, nameUTF8); - setExecutable(execute); - } - - public FileMode getMode() { - return mode; - } - - /** - * @return true if this file is executable - */ - public boolean isExecutable() { - return getMode().equals(FileMode.EXECUTABLE_FILE); - } - - /** - * @param execute set/reset the executable flag - */ - public void setExecutable(final boolean execute) { - mode = execute ? FileMode.EXECUTABLE_FILE : FileMode.REGULAR_FILE; - } - - /** - * @return an {@link ObjectLoader} that will return the data - * @throws IOException - */ - public ObjectLoader openReader() throws IOException { - return getRepository().open(getId(), Constants.OBJ_BLOB); - } - - public String toString() { - final StringBuilder r = new StringBuilder(); - r.append(ObjectId.toString(getId())); - r.append(' '); - r.append(isExecutable() ? 'X' : 'F'); - r.append(' '); - r.append(getFullName()); - return r.toString(); - } -} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Tree.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Tree.java deleted file mode 100644 index 43bd489dc0..0000000000 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Tree.java +++ /dev/null @@ -1,601 +0,0 @@ -/* - * Copyright (C) 2007, Robin Rosenberg <me@lathund.dewire.com> - * Copyright (C) 2007-2008, Robin Rosenberg <robin.rosenberg@dewire.com> - * Copyright (C) 2006-2008, Shawn O. Pearce <spearce@spearce.org> - * 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.lib; - -import java.io.IOException; -import java.text.MessageFormat; - -import org.eclipse.jgit.errors.CorruptObjectException; -import org.eclipse.jgit.errors.EntryExistsException; -import org.eclipse.jgit.errors.MissingObjectException; -import org.eclipse.jgit.errors.ObjectWritingException; -import org.eclipse.jgit.internal.JGitText; -import org.eclipse.jgit.util.RawParseUtils; - -/** - * A representation of a Git tree entry. A Tree is a directory in Git. - * - * @deprecated To look up information about a single path, use - * {@link org.eclipse.jgit.treewalk.TreeWalk#forPath(Repository, String, org.eclipse.jgit.revwalk.RevTree)}. - * To lookup information about multiple paths at once, use a - * {@link org.eclipse.jgit.treewalk.TreeWalk} and obtain the current entry's - * information from its getter methods. - */ -@Deprecated -public class Tree extends TreeEntry { - private static final TreeEntry[] EMPTY_TREE = {}; - - /** - * Compare two names represented as bytes. Since git treats names of trees and - * blobs differently we have one parameter that represents a '/' for trees. For - * other objects the value should be NUL. The names are compare by their positive - * byte value (0..255). - * - * A blob and a tree with the same name will not compare equal. - * - * @param a name - * @param b name - * @param lasta '/' if a is a tree, else NUL - * @param lastb '/' if b is a tree, else NUL - * - * @return < 0 if a is sorted before b, 0 if they are the same, else b - */ - public static final int compareNames(final byte[] a, final byte[] b, final int lasta,final int lastb) { - return compareNames(a, b, 0, b.length, lasta, lastb); - } - - private static final int compareNames(final byte[] a, final byte[] nameUTF8, - final int nameStart, final int nameEnd, final int lasta, int lastb) { - int j,k; - for (j = 0, k = nameStart; j < a.length && k < nameEnd; j++, k++) { - final int aj = a[j] & 0xff; - final int bk = nameUTF8[k] & 0xff; - if (aj < bk) - return -1; - else if (aj > bk) - return 1; - } - if (j < a.length) { - int aj = a[j]&0xff; - if (aj < lastb) - return -1; - else if (aj > lastb) - return 1; - else - if (j == a.length - 1) - return 0; - else - return -1; - } - if (k < nameEnd) { - int bk = nameUTF8[k] & 0xff; - if (lasta < bk) - return -1; - else if (lasta > bk) - return 1; - else - if (k == nameEnd - 1) - return 0; - else - return 1; - } - if (lasta < lastb) - return -1; - else if (lasta > lastb) - return 1; - - final int namelength = nameEnd - nameStart; - if (a.length == namelength) - return 0; - else if (a.length < namelength) - return -1; - else - return 1; - } - - private static final byte[] substring(final byte[] s, final int nameStart, - final int nameEnd) { - if (nameStart == 0 && nameStart == s.length) - return s; - final byte[] n = new byte[nameEnd - nameStart]; - System.arraycopy(s, nameStart, n, 0, n.length); - return n; - } - - private static final int binarySearch(final TreeEntry[] entries, - final byte[] nameUTF8, final int nameUTF8last, final int nameStart, final int nameEnd) { - if (entries.length == 0) - return -1; - int high = entries.length; - int low = 0; - do { - final int mid = (low + high) >>> 1; - final int cmp = compareNames(entries[mid].getNameUTF8(), nameUTF8, - nameStart, nameEnd, TreeEntry.lastChar(entries[mid]), nameUTF8last); - if (cmp < 0) - low = mid + 1; - else if (cmp == 0) - return mid; - else - high = mid; - } while (low < high); - return -(low + 1); - } - - private final Repository db; - - private TreeEntry[] contents; - - /** - * Constructor for a new Tree - * - * @param repo The repository that owns the Tree. - */ - public Tree(final Repository repo) { - super(null, null, null); - db = repo; - contents = EMPTY_TREE; - } - - /** - * Construct a Tree object with known content and hash value - * - * @param repo - * @param myId - * @param raw - * @throws IOException - */ - public Tree(final Repository repo, final ObjectId myId, final byte[] raw) - throws IOException { - super(null, myId, null); - db = repo; - readTree(raw); - } - - /** - * Construct a new Tree under another Tree - * - * @param parent - * @param nameUTF8 - */ - public Tree(final Tree parent, final byte[] nameUTF8) { - super(parent, null, nameUTF8); - db = parent.getRepository(); - contents = EMPTY_TREE; - } - - /** - * Construct a Tree with a known SHA-1 under another tree. Data is not yet - * specified and will have to be loaded on demand. - * - * @param parent - * @param id - * @param nameUTF8 - */ - public Tree(final Tree parent, final ObjectId id, final byte[] nameUTF8) { - super(parent, id, nameUTF8); - db = parent.getRepository(); - } - - public FileMode getMode() { - return FileMode.TREE; - } - - /** - * @return true if this Tree is the top level Tree. - */ - public boolean isRoot() { - return getParent() == null; - } - - public Repository getRepository() { - return db; - } - - /** - * @return true of the data of this Tree is loaded - */ - public boolean isLoaded() { - return contents != null; - } - - /** - * Forget the in-memory data for this tree. - */ - public void unload() { - if (isModified()) - throw new IllegalStateException(JGitText.get().cannotUnloadAModifiedTree); - contents = null; - } - - /** - * Adds a new or existing file with the specified name to this tree. - * Trees are added if necessary as the name may contain '/':s. - * - * @param name Name - * @return a {@link FileTreeEntry} for the added file. - * @throws IOException - */ - public FileTreeEntry addFile(final String name) throws IOException { - return addFile(Repository.gitInternalSlash(Constants.encode(name)), 0); - } - - /** - * Adds a new or existing file with the specified name to this tree. - * Trees are added if necessary as the name may contain '/':s. - * - * @param s an array containing the name - * @param offset when the name starts in the tree. - * - * @return a {@link FileTreeEntry} for the added file. - * @throws IOException - */ - public FileTreeEntry addFile(final byte[] s, final int offset) - throws IOException { - int slash; - int p; - - for (slash = offset; slash < s.length && s[slash] != '/'; slash++) { - // search for path component terminator - } - - ensureLoaded(); - byte xlast = slash<s.length ? (byte)'/' : 0; - p = binarySearch(contents, s, xlast, offset, slash); - if (p >= 0 && slash < s.length && contents[p] instanceof Tree) - return ((Tree) contents[p]).addFile(s, slash + 1); - - final byte[] newName = substring(s, offset, slash); - if (p >= 0) - throw new EntryExistsException(RawParseUtils.decode(newName)); - else if (slash < s.length) { - final Tree t = new Tree(this, newName); - insertEntry(p, t); - return t.addFile(s, slash + 1); - } else { - final FileTreeEntry f = new FileTreeEntry(this, null, newName, - false); - insertEntry(p, f); - return f; - } - } - - /** - * Adds a new or existing Tree with the specified name to this tree. - * Trees are added if necessary as the name may contain '/':s. - * - * @param name Name - * @return a {@link FileTreeEntry} for the added tree. - * @throws IOException - */ - public Tree addTree(final String name) throws IOException { - return addTree(Repository.gitInternalSlash(Constants.encode(name)), 0); - } - - /** - * Adds a new or existing Tree with the specified name to this tree. - * Trees are added if necessary as the name may contain '/':s. - * - * @param s an array containing the name - * @param offset when the name starts in the tree. - * - * @return a {@link FileTreeEntry} for the added tree. - * @throws IOException - */ - public Tree addTree(final byte[] s, final int offset) throws IOException { - int slash; - int p; - - for (slash = offset; slash < s.length && s[slash] != '/'; slash++) { - // search for path component terminator - } - - ensureLoaded(); - p = binarySearch(contents, s, (byte)'/', offset, slash); - if (p >= 0 && slash < s.length && contents[p] instanceof Tree) - return ((Tree) contents[p]).addTree(s, slash + 1); - - final byte[] newName = substring(s, offset, slash); - if (p >= 0) - throw new EntryExistsException(RawParseUtils.decode(newName)); - - final Tree t = new Tree(this, newName); - insertEntry(p, t); - return slash == s.length ? t : t.addTree(s, slash + 1); - } - - /** - * Add the specified tree entry to this tree. - * - * @param e - * @throws IOException - */ - public void addEntry(final TreeEntry e) throws IOException { - final int p; - - ensureLoaded(); - p = binarySearch(contents, e.getNameUTF8(), TreeEntry.lastChar(e), 0, e.getNameUTF8().length); - if (p < 0) { - e.attachParent(this); - insertEntry(p, e); - } else { - throw new EntryExistsException(e.getName()); - } - } - - private void insertEntry(int p, final TreeEntry e) { - final TreeEntry[] c = contents; - final TreeEntry[] n = new TreeEntry[c.length + 1]; - p = -(p + 1); - for (int k = c.length - 1; k >= p; k--) - n[k + 1] = c[k]; - n[p] = e; - for (int k = p - 1; k >= 0; k--) - n[k] = c[k]; - contents = n; - setModified(); - } - - void removeEntry(final TreeEntry e) { - final TreeEntry[] c = contents; - final int p = binarySearch(c, e.getNameUTF8(), TreeEntry.lastChar(e), 0, - e.getNameUTF8().length); - if (p >= 0) { - final TreeEntry[] n = new TreeEntry[c.length - 1]; - for (int k = c.length - 1; k > p; k--) - n[k - 1] = c[k]; - for (int k = p - 1; k >= 0; k--) - n[k] = c[k]; - contents = n; - setModified(); - } - } - - /** - * @return number of members in this tree - * @throws IOException - */ - public int memberCount() throws IOException { - ensureLoaded(); - return contents.length; - } - - /** - * Return all members of the tree sorted in Git order. - * - * Entries are sorted by the numerical unsigned byte - * values with (sub)trees having an implicit '/'. An - * example of a tree with three entries. a:b is an - * actual file name here. - * - * <p> - * 100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a.b - * 040000 tree 4277b6e69d25e5efa77c455340557b384a4c018a a - * 100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a:b - * - * @return all entries in this Tree, sorted. - * @throws IOException - */ - public TreeEntry[] members() throws IOException { - ensureLoaded(); - final TreeEntry[] c = contents; - if (c.length != 0) { - final TreeEntry[] r = new TreeEntry[c.length]; - for (int k = c.length - 1; k >= 0; k--) - r[k] = c[k]; - return r; - } else - return c; - } - - private boolean exists(final String s, byte slast) throws IOException { - return findMember(s, slast) != null; - } - - /** - * @param path to the tree. - * @return true if a tree with the specified path can be found under this - * tree. - * @throws IOException - */ - public boolean existsTree(String path) throws IOException { - return exists(path,(byte)'/'); - } - - /** - * @param path of the non-tree entry. - * @return true if a blob, symlink, or gitlink with the specified name - * can be found under this tree. - * @throws IOException - */ - public boolean existsBlob(String path) throws IOException { - return exists(path,(byte)0); - } - - private TreeEntry findMember(final String s, byte slast) throws IOException { - return findMember(Repository.gitInternalSlash(Constants.encode(s)), slast, 0); - } - - private TreeEntry findMember(final byte[] s, final byte slast, final int offset) - throws IOException { - int slash; - int p; - - for (slash = offset; slash < s.length && s[slash] != '/'; slash++) { - // search for path component terminator - } - - ensureLoaded(); - byte xlast = slash<s.length ? (byte)'/' : slast; - p = binarySearch(contents, s, xlast, offset, slash); - if (p >= 0) { - final TreeEntry r = contents[p]; - if (slash < s.length-1) - return r instanceof Tree ? ((Tree) r).findMember(s, slast, slash + 1) - : null; - return r; - } - return null; - } - - /** - * @param s - * blob name - * @return a {@link TreeEntry} representing an object with the specified - * relative path. - * @throws IOException - */ - public TreeEntry findBlobMember(String s) throws IOException { - return findMember(s,(byte)0); - } - - /** - * @param s Tree Name - * @return a Tree with the name s or null - * @throws IOException - */ - public TreeEntry findTreeMember(String s) throws IOException { - return findMember(s,(byte)'/'); - } - - private void ensureLoaded() throws IOException, MissingObjectException { - if (!isLoaded()) { - ObjectLoader ldr = db.open(getId(), Constants.OBJ_TREE); - readTree(ldr.getCachedBytes()); - } - } - - private void readTree(final byte[] raw) throws IOException { - final int rawSize = raw.length; - int rawPtr = 0; - TreeEntry[] temp; - int nextIndex = 0; - - while (rawPtr < rawSize) { - while (rawPtr < rawSize && raw[rawPtr] != 0) - rawPtr++; - rawPtr++; - rawPtr += Constants.OBJECT_ID_LENGTH; - nextIndex++; - } - - temp = new TreeEntry[nextIndex]; - rawPtr = 0; - nextIndex = 0; - while (rawPtr < rawSize) { - int c = raw[rawPtr++]; - if (c < '0' || c > '7') - throw new CorruptObjectException(getId(), JGitText.get().corruptObjectInvalidEntryMode); - int mode = c - '0'; - for (;;) { - c = raw[rawPtr++]; - if (' ' == c) - break; - else if (c < '0' || c > '7') - throw new CorruptObjectException(getId(), JGitText.get().corruptObjectInvalidMode); - mode <<= 3; - mode += c - '0'; - } - - int nameLen = 0; - while (raw[rawPtr + nameLen] != 0) - nameLen++; - final byte[] name = new byte[nameLen]; - System.arraycopy(raw, rawPtr, name, 0, nameLen); - rawPtr += nameLen + 1; - - final ObjectId id = ObjectId.fromRaw(raw, rawPtr); - rawPtr += Constants.OBJECT_ID_LENGTH; - - final TreeEntry ent; - if (FileMode.REGULAR_FILE.equals(mode)) - ent = new FileTreeEntry(this, id, name, false); - else if (FileMode.EXECUTABLE_FILE.equals(mode)) - ent = new FileTreeEntry(this, id, name, true); - else if (FileMode.TREE.equals(mode)) - ent = new Tree(this, id, name); - else if (FileMode.SYMLINK.equals(mode)) - ent = new SymlinkTreeEntry(this, id, name); - else if (FileMode.GITLINK.equals(mode)) - ent = new GitlinkTreeEntry(this, id, name); - else - throw new CorruptObjectException(getId(), MessageFormat.format( - JGitText.get().corruptObjectInvalidMode2, Integer.toOctalString(mode))); - temp[nextIndex++] = ent; - } - - contents = temp; - } - - /** - * Format this Tree in canonical format. - * - * @return canonical encoding of the tree object. - * @throws IOException - * the tree cannot be loaded, or its not in a writable state. - */ - public byte[] format() throws IOException { - TreeFormatter fmt = new TreeFormatter(); - for (TreeEntry e : members()) { - ObjectId id = e.getId(); - if (id == null) - throw new ObjectWritingException(MessageFormat.format(JGitText - .get().objectAtPathDoesNotHaveId, e.getFullName())); - - fmt.append(e.getNameUTF8(), e.getMode(), id); - } - return fmt.toByteArray(); - } - - public String toString() { - final StringBuilder r = new StringBuilder(); - r.append(ObjectId.toString(getId())); - r.append(" T "); //$NON-NLS-1$ - r.append(getFullName()); - return r.toString(); - } - -} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/TreeEntry.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/TreeEntry.java deleted file mode 100644 index a1ffa68056..0000000000 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/TreeEntry.java +++ /dev/null @@ -1,256 +0,0 @@ -/* - * Copyright (C) 2007-2008, Robin Rosenberg <robin.rosenberg@dewire.com> - * Copyright (C) 2006-2007, Shawn O. Pearce <spearce@spearce.org> - * 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.lib; - -import java.io.IOException; - -import org.eclipse.jgit.util.RawParseUtils; - -/** - * This class represents an entry in a tree, like a blob or another tree. - * - * @deprecated To look up information about a single path, use - * {@link org.eclipse.jgit.treewalk.TreeWalk#forPath(Repository, String, org.eclipse.jgit.revwalk.RevTree)}. - * To lookup information about multiple paths at once, use a - * {@link org.eclipse.jgit.treewalk.TreeWalk} and obtain the current entry's - * information from its getter methods. - */ -@Deprecated -public abstract class TreeEntry implements Comparable { - private byte[] nameUTF8; - - private Tree parent; - - private ObjectId id; - - /** - * Construct a named tree entry. - * - * @param myParent - * @param myId - * @param myNameUTF8 - */ - protected TreeEntry(final Tree myParent, final ObjectId myId, - final byte[] myNameUTF8) { - nameUTF8 = myNameUTF8; - parent = myParent; - id = myId; - } - - /** - * @return parent of this tree. - */ - public Tree getParent() { - return parent; - } - - /** - * Delete this entry. - */ - public void delete() { - getParent().removeEntry(this); - detachParent(); - } - - /** - * Detach this entry from it's parent. - */ - public void detachParent() { - parent = null; - } - - void attachParent(final Tree p) { - parent = p; - } - - /** - * @return the repository owning this entry. - */ - public Repository getRepository() { - return getParent().getRepository(); - } - - /** - * @return the raw byte name of this entry. - */ - public byte[] getNameUTF8() { - return nameUTF8; - } - - /** - * @return the name of this entry. - */ - public String getName() { - if (nameUTF8 != null) - return RawParseUtils.decode(nameUTF8); - return null; - } - - /** - * Rename this entry. - * - * @param n The new name - * @throws IOException - */ - public void rename(final String n) throws IOException { - rename(Constants.encode(n)); - } - - /** - * Rename this entry. - * - * @param n The new name - * @throws IOException - */ - public void rename(final byte[] n) throws IOException { - final Tree t = getParent(); - if (t != null) { - delete(); - } - nameUTF8 = n; - if (t != null) { - t.addEntry(this); - } - } - - /** - * @return true if this entry is new or modified since being loaded. - */ - public boolean isModified() { - return getId() == null; - } - - /** - * Mark this entry as modified. - */ - public void setModified() { - setId(null); - } - - /** - * @return SHA-1 of this tree entry (null for new unhashed entries) - */ - public ObjectId getId() { - return id; - } - - /** - * Set (update) the SHA-1 of this entry. Invalidates the id's of all - * entries above this entry as they will have to be recomputed. - * - * @param n SHA-1 for this entry. - */ - public void setId(final ObjectId n) { - // If we have a parent and our id is being cleared or changed then force - // the parent's id to become unset as it depends on our id. - // - final Tree p = getParent(); - if (p != null && id != n) { - if ((id == null && n != null) || (id != null && n == null) - || !id.equals(n)) { - p.setId(null); - } - } - - id = n; - } - - /** - * @return repository relative name of this entry - */ - public String getFullName() { - final StringBuilder r = new StringBuilder(); - appendFullName(r); - return r.toString(); - } - - /** - * @return repository relative name of the entry - * FIXME better encoding - */ - public byte[] getFullNameUTF8() { - return getFullName().getBytes(); - } - - public int compareTo(final Object o) { - if (this == o) - return 0; - if (o instanceof TreeEntry) - return Tree.compareNames(nameUTF8, ((TreeEntry) o).nameUTF8, lastChar(this), lastChar((TreeEntry)o)); - return -1; - } - - /** - * Helper for accessing tree/blob methods. - * - * @param treeEntry - * @return '/' for Tree entries and NUL for non-treeish objects. - */ - final public static int lastChar(TreeEntry treeEntry) { - if (!(treeEntry instanceof Tree)) - return '\0'; - else - return '/'; - } - - /** - * @return mode (type of object) - */ - public abstract FileMode getMode(); - - private void appendFullName(final StringBuilder r) { - final TreeEntry p = getParent(); - final String n = getName(); - if (p != null) { - p.appendFullName(r); - if (r.length() > 0) { - r.append('/'); - } - } - if (n != null) { - r.append(n); - } - } -} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseReceivePack.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseReceivePack.java index 728643e925..a20e652553 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseReceivePack.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseReceivePack.java @@ -1453,10 +1453,7 @@ public abstract class BaseReceivePack { * @since 3.6 */ protected void failPendingCommands() { - for (ReceiveCommand cmd : commands) { - if (cmd.getResult() == Result.NOT_ATTEMPTED) - cmd.setResult(Result.REJECTED_OTHER_REASON, JGitText.get().transactionAborted); - } + ReceiveCommand.abort(commands); } /** diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/Connection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/Connection.java index 0ff9fcea74..da288ec31e 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/Connection.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/Connection.java @@ -59,8 +59,7 @@ import org.eclipse.jgit.lib.Ref; * * @see Transport */ -public interface Connection { - +public interface Connection extends AutoCloseable { /** * Get the complete map of refs advertised as available for fetching or * pushing. @@ -108,6 +107,10 @@ public interface Connection { * <p> * If additional messages were produced by the remote peer, these should * still be retained in the connection instance for {@link #getMessages()}. + * <p> + * {@code AutoClosable.close()} declares that it throws {@link Exception}. + * Implementers shouldn't throw checked exceptions. This override narrows + * the signature to prevent them from doing so. */ public void close(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/Daemon.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/Daemon.java index d9e0b937e8..2593ba556d 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/Daemon.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/Daemon.java @@ -256,6 +256,16 @@ public class Daemon { } /** + * Get the factory used to construct per-request ReceivePack. + * + * @return the factory. + * @since 4.2 + */ + public ReceivePackFactory<DaemonClient> getReceivePackFactory() { + return receivePackFactory; + } + + /** * Set the factory to construct and configure per-request ReceivePack. * * @param factory diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceiveCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceiveCommand.java index 5702b6d7b9..2b21c4a8fe 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceiveCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceiveCommand.java @@ -43,6 +43,9 @@ package org.eclipse.jgit.transport; +import static org.eclipse.jgit.transport.ReceiveCommand.Result.NOT_ATTEMPTED; +import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_OTHER_REASON; + import java.io.IOException; import java.text.MessageFormat; import java.util.ArrayList; @@ -168,6 +171,25 @@ public class ReceiveCommand { return filter((Iterable<ReceiveCommand>) commands, want); } + /** + * Set unprocessed commands as failed due to transaction aborted. + * <p> + * If a command is still {@link Result#NOT_ATTEMPTED} it will be set to + * {@link Result#REJECTED_OTHER_REASON}. + * + * @param commands + * commands to mark as failed. + * @since 4.2 + */ + public static void abort(Iterable<ReceiveCommand> commands) { + for (ReceiveCommand c : commands) { + if (c.getResult() == NOT_ATTEMPTED) { + c.setResult(REJECTED_OTHER_REASON, + JGitText.get().transactionAborted); + } + } + } + private final ObjectId oldId; private final ObjectId newId; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/Transport.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/Transport.java index 6af153cbc9..9e6d1f68f5 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/Transport.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/Transport.java @@ -98,7 +98,7 @@ import org.eclipse.jgit.storage.pack.PackConfig; * Transport instances and the connections they create are not thread-safe. * Callers must ensure a transport is accessed by only one thread at a time. */ -public abstract class Transport { +public abstract class Transport implements AutoCloseable { /** Type of operation a Transport is being opened for. */ public enum Operation { /** Transport is to fetch objects locally. */ @@ -1353,6 +1353,10 @@ public abstract class Transport { * must close that network socket, disconnecting the two peers. If the * remote repository is actually local (same system) this method must close * any open file handles used to read the "remote" repository. + * <p> + * {@code AutoClosable.close()} declares that it throws {@link Exception}. + * Implementers shouldn't throw checked exceptions. This override narrows + * the signature to prevent them from doing so. */ public abstract void close(); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/URIish.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/URIish.java index 5ce39c2e03..3ee2feb140 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/URIish.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/URIish.java @@ -715,7 +715,7 @@ public class URIish implements Serializable { */ public String getHumanishName() throws IllegalArgumentException { String s = getPath(); - if ("/".equals(s) || "".equals(s)) //$NON-NLS-1$ + if ("/".equals(s) || "".equals(s)) //$NON-NLS-1$ //$NON-NLS-2$ s = getHost(); if (s == null) // $NON-NLS-1$ throw new IllegalArgumentException(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/TreeWalk.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/TreeWalk.java index 83fada4f95..5cd713da78 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/TreeWalk.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/TreeWalk.java @@ -574,18 +574,13 @@ public class TreeWalk implements AutoCloseable, AttributesProvider { * @param p * an iterator to walk over. The iterator should be new, with no * parent, and should still be positioned before the first entry. - * The tree which the iterator operates on must have the same root - * as other trees in the walk. - * + * The tree which the iterator operates on must have the same + * root as other trees in the walk. * @return position of this tree within the walker. - * @throws CorruptObjectException - * the iterator was unable to obtain its first entry, due to - * possible data corruption within the backing data store. */ - public int addTree(final AbstractTreeIterator p) - throws CorruptObjectException { - final int n = trees.length; - final AbstractTreeIterator[] newTrees = new AbstractTreeIterator[n + 1]; + public int addTree(AbstractTreeIterator p) { + int n = trees.length; + AbstractTreeIterator[] newTrees = new AbstractTreeIterator[n + 1]; System.arraycopy(trees, 0, newTrees, 0, n); newTrees[n] = p; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/ChangeIdUtil.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/ChangeIdUtil.java index 35fc99e54e..e14096e598 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/ChangeIdUtil.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/ChangeIdUtil.java @@ -42,7 +42,6 @@ */ package org.eclipse.jgit.util; -import java.io.IOException; import java.util.regex.Pattern; import org.eclipse.jgit.lib.Constants; @@ -90,12 +89,10 @@ public class ChangeIdUtil { * The commit message * @return the change id SHA1 string (without the 'I') or null if the * message is not complete enough - * @throws IOException */ public static ObjectId computeChangeId(final ObjectId treeId, final ObjectId firstParentId, final PersonIdent author, - final PersonIdent committer, final String message) - throws IOException { + final PersonIdent committer, final String message) { String cleanMessage = clean(message); if (cleanMessage.length() == 0) return null; @@ -116,8 +113,7 @@ public class ChangeIdUtil { b.append("\n\n"); //$NON-NLS-1$ b.append(cleanMessage); try (ObjectInserter f = new ObjectInserter.Formatter()) { - return f.idFor(Constants.OBJ_COMMIT, // - b.toString().getBytes(Constants.CHARACTER_ENCODING)); + return f.idFor(Constants.OBJ_COMMIT, Constants.encode(b.toString())); } } |