org.eclipse.jgit.treewalk.filter;version="[4.6.0,4.7.0)",
org.eclipse.jgit.util;version="[4.6.0,4.7.0)",
org.eclipse.jgit.util.io;version="[4.6.0,4.7.0)",
+ org.eclipse.jgit.util.time;version="[4.6.0,4.7.0)",
org.junit;version="[4.0.0,5.0.0)",
org.junit.rules;version="[4.9.0,5.0.0)",
org.junit.runner;version="[4.0.0,5.0.0)",
org.eclipse.jgit.treewalk,
org.eclipse.jgit.util,
org.eclipse.jgit.storage.file,
- org.eclipse.jgit.api"
+ org.eclipse.jgit.api",
+ org.eclipse.jgit.junit.time;version="4.6.0"
ceilTestDirectories(getCeilings());
SystemReader.setInstance(mockSystemReader);
- final long now = mockSystemReader.getCurrentTime();
- final int tz = mockSystemReader.getTimezone(now);
author = new PersonIdent("J. Author", "jauthor@example.com");
- author = new PersonIdent(author, now, tz);
-
committer = new PersonIdent("J. Committer", "jcommitter@example.com");
- committer = new PersonIdent(committer, now, tz);
final WindowCacheConfig c = new WindowCacheConfig();
c.setPackedGitLimit(128 * WindowCacheConfig.KB);
import java.lang.reflect.Field;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
+import java.time.Duration;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
+import java.util.concurrent.TimeUnit;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.storage.file.FileBasedConfig;
import org.eclipse.jgit.util.FS;
import org.eclipse.jgit.util.SystemReader;
+import org.eclipse.jgit.util.time.MonotonicClock;
+import org.eclipse.jgit.util.time.ProposedTimestamp;
/**
* Mock {@link SystemReader} for tests.
return now;
}
+ @Override
+ public MonotonicClock getClock() {
+ return new MonotonicClock() {
+ @Override
+ public ProposedTimestamp propose() {
+ long t = getCurrentTime();
+ return new ProposedTimestamp() {
+ @Override
+ public long read(TimeUnit unit) {
+ return unit.convert(t, TimeUnit.MILLISECONDS);
+ }
+
+ @Override
+ public void blockUntil(Duration maxWait) {
+ // Do not wait.
+ }
+ };
+ }
+ };
+ }
+
/**
* Adjusts the current time in seconds.
*
--- /dev/null
+/*
+ * 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.junit.time;
+
+import java.time.Duration;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jgit.util.time.MonotonicClock;
+import org.eclipse.jgit.util.time.ProposedTimestamp;
+
+/**
+ * Fake {@link MonotonicClock} for testing code that uses Clock.
+ *
+ * @since 4.6
+ */
+public class MonotonicFakeClock implements MonotonicClock {
+ private long now = TimeUnit.SECONDS.toMicros(42);
+
+ /**
+ * Advance the time returned by future calls to {@link #propose()}.
+ *
+ * @param add
+ * amount of time to add; must be {@code > 0}.
+ * @param unit
+ * unit of {@code add}.
+ */
+ public void tick(long add, TimeUnit unit) {
+ if (add <= 0) {
+ throw new IllegalArgumentException();
+ }
+ now += unit.toMillis(add);
+ }
+
+ @Override
+ public ProposedTimestamp propose() {
+ long t = now++;
+ return new ProposedTimestamp() {
+ @Override
+ public long read(TimeUnit unit) {
+ return unit.convert(t, TimeUnit.MILLISECONDS);
+ }
+
+ @Override
+ public void blockUntil(Duration maxWait) {
+ // Nothing to do, since fake time does not go backwards.
+ }
+ };
+ }
+}
org.eclipse.jgit.ignore.internal;version="4.6.0";x-friends:="org.eclipse.jgit.test",
org.eclipse.jgit.internal;version="4.6.0";x-friends:="org.eclipse.jgit.test,org.eclipse.jgit.http.test",
org.eclipse.jgit.internal.ketch;version="4.6.0";x-friends:="org.eclipse.jgit.junit,org.eclipse.jgit.test,org.eclipse.jgit.pgm",
- org.eclipse.jgit.internal.storage.dfs;version="4.6.0";
- x-friends:="org.eclipse.jgit.test,
- org.eclipse.jgit.http.server,
- org.eclipse.jgit.http.test",
+ org.eclipse.jgit.internal.storage.dfs;version="4.6.0";x-friends:="org.eclipse.jgit.test,org.eclipse.jgit.http.server,org.eclipse.jgit.http.test",
org.eclipse.jgit.internal.storage.file;version="4.6.0";
x-friends:="org.eclipse.jgit.test,
org.eclipse.jgit.junit,
org.eclipse.jgit.transport.http,
org.eclipse.jgit.storage.file,
org.ietf.jgss",
- org.eclipse.jgit.util.io;version="4.6.0"
+ org.eclipse.jgit.util.io;version="4.6.0",
+ org.eclipse.jgit.util.time;version="4.6.0"
Bundle-RequiredExecutionEnvironment: JavaSE-1.8
Import-Package: com.googlecode.javaewah;version="[1.1.6,2.0.0)",
com.jcraft.jsch;version="[0.1.37,0.2.0)",
tagNameInvalid=tag name {0} is invalid
tagOnRepoWithoutHEADCurrentlyNotSupported=Tag on repository without HEAD currently not supported
theFactoryMustNotBeNull=The factory must not be null
+timeIsUncertain=Time is uncertain
timerAlreadyTerminated=Timer already terminated
tooManyIncludeRecursions=Too many recursions; circular includes in config file(s)?
topologicalSortRequired=Topological sort required.
/***/ public String tagOnRepoWithoutHEADCurrentlyNotSupported;
/***/ public String transactionAborted;
/***/ public String theFactoryMustNotBeNull;
+ /***/ public String timeIsUncertain;
/***/ public String timerAlreadyTerminated;
/***/ public String tooManyIncludeRecursions;
/***/ public String topologicalSortRequired;
package org.eclipse.jgit.internal.ketch;
+import static java.util.concurrent.TimeUnit.SECONDS;
import static org.eclipse.jgit.internal.ketch.KetchConstants.TERM;
import java.io.IOException;
import java.util.List;
+import java.util.concurrent.TimeoutException;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.TreeFormatter;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.util.time.ProposedTimestamp;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
void start() throws IOException {
ObjectId id;
try (Repository git = leader.openRepository();
+ ProposedTimestamp ts = getSystem().getClock().propose();
ObjectInserter inserter = git.newObjectInserter()) {
- id = bumpTerm(git, inserter);
+ id = bumpTerm(git, ts, inserter);
inserter.flush();
+ blockUntil(ts);
}
runAsync(id);
}
return term;
}
- private ObjectId bumpTerm(Repository git, ObjectInserter inserter)
- throws IOException {
+ private ObjectId bumpTerm(Repository git, ProposedTimestamp ts,
+ ObjectInserter inserter) throws IOException {
CommitBuilder b = new CommitBuilder();
if (!ObjectId.zeroId().equals(acceptedOldIndex)) {
try (RevWalk rw = new RevWalk(git)) {
RevCommit c = rw.parseCommit(acceptedOldIndex);
+ if (getSystem().requireMonotonicLeaderElections()) {
+ if (ts.read(SECONDS) < c.getCommitTime()) {
+ throw new TimeIsUncertainException();
+ }
+ }
b.setTreeId(c.getTree());
b.setParentId(acceptedOldIndex);
term = parseTerm(c.getFooterLines(TERM)) + 1;
msg.append(' ').append(tag);
}
- b.setAuthor(leader.getSystem().newCommitter());
+ b.setAuthor(leader.getSystem().newCommitter(ts));
b.setCommitter(b.getAuthor());
b.setMessage(msg.toString());
}
return Long.parseLong(s, 10);
}
+
+ private void blockUntil(ProposedTimestamp ts) throws IOException {
+ try {
+ ts.blockUntil(getSystem().getMaxWaitForMonotonicClock());
+ } catch (InterruptedException | TimeoutException e) {
+ throw new TimeIsUncertainException(e);
+ }
+ }
}
import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_REMOTE;
import java.net.URISyntaxException;
+import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.transport.RemoteConfig;
import org.eclipse.jgit.transport.URIish;
+import org.eclipse.jgit.util.time.MonotonicClock;
+import org.eclipse.jgit.util.time.MonotonicSystemClock;
+import org.eclipse.jgit.util.time.ProposedTimestamp;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
}
private final ScheduledExecutorService executor;
+ private final MonotonicClock clock;
private final String txnNamespace;
private final String txnAccepted;
private final String txnCommitted;
/** Create a default system with a thread pool of 1 thread per CPU. */
public KetchSystem() {
- this(defaultExecutor(), DEFAULT_TXN_NAMESPACE);
+ this(defaultExecutor(), new MonotonicSystemClock(), DEFAULT_TXN_NAMESPACE);
}
/**
*
* @param executor
* thread pool to run background operations.
+ * @param clock
+ * clock to create timestamps.
* @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) {
+ public KetchSystem(ScheduledExecutorService executor, MonotonicClock clock,
+ String txnNamespace) {
this.executor = executor;
+ this.clock = clock;
this.txnNamespace = txnNamespace;
this.txnAccepted = txnNamespace + ACCEPTED;
this.txnCommitted = txnNamespace + COMMITTED;
return executor;
}
+ /** @return clock to obtain timestamps from. */
+ public MonotonicClock getClock() {
+ return clock;
+ }
+
+ /**
+ * @return how long the leader will wait for the {@link #getClock()}'s
+ * {@code ProposedTimestamp} used in commits proposed to the RefTree
+ * graph ({@link #getTxnAccepted()}). Defaults to 5 seconds.
+ */
+ public Duration getMaxWaitForMonotonicClock() {
+ return Duration.ofSeconds(5);
+ }
+
+ /**
+ * @return true if elections should require monotonically increasing commit
+ * timestamps. This requires a very good {@link MonotonicClock}.
+ */
+ public boolean requireMonotonicLeaderElections() {
+ return false;
+ }
+
/**
* Get the namespace used for the RefTree graph and transaction management.
*
return txnStage;
}
- /** @return identity line for the committer header of a RefTreeGraph. */
- public PersonIdent newCommitter() {
+ /**
+ * @param time
+ * timestamp for the committer.
+ * @return identity line for the committer header of a RefTreeGraph.
+ */
+ public PersonIdent newCommitter(ProposedTimestamp time) {
String name = "ketch"; //$NON-NLS-1$
String email = "ketch@system"; //$NON-NLS-1$
- return new PersonIdent(name, email);
+ return new PersonIdent(name, email, time);
}
/**
* 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.
+ * time (rounded to seconds) using the same
+ * {@link #newCommitter(ProposedTimestamp)} 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.
+ * {@link #newCommitter(ProposedTimestamp)}, 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.
*
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.ReceiveCommand;
+import org.eclipse.jgit.util.time.MonotonicClock;
+import org.eclipse.jgit.util.time.ProposedTimestamp;
/** Ketch replica running on the same system as the {@link KetchLeader}. */
public class LocalReplica extends KetchReplica {
getSystem().getExecutor().execute(new Runnable() {
@Override
public void run() {
- try (Repository git = getLeader().openRepository()) {
+ MonotonicClock clk = getSystem().getClock();
+ try (Repository git = getLeader().openRepository();
+ ProposedTimestamp ts = clk.propose()) {
try {
- update(git, req);
+ update(git, req, ts);
req.done(git);
} catch (Throwable err) {
req.setException(git, err);
throw new IOException(KetchText.get().cannotFetchFromLocalReplica);
}
- private void update(Repository git, ReplicaPushRequest req)
- throws IOException {
+ private void update(Repository git, ReplicaPushRequest req,
+ ProposedTimestamp ts) throws IOException {
RefDatabase refdb = git.getRefDatabase();
CommitMethod method = getCommitMethod();
}
BatchRefUpdate batch = refdb.newBatchUpdate();
- batch.setRefLogIdent(getSystem().newCommitter());
+ batch.addProposedTimestamp(ts);
+ batch.setRefLogIdent(getSystem().newCommitter(ts));
batch.setRefLogMessage("ketch", false); //$NON-NLS-1$
batch.setAllowNonFastForwards(true);
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.PushCertificate;
import org.eclipse.jgit.transport.ReceiveCommand;
+import org.eclipse.jgit.util.time.ProposedTimestamp;
/**
* A proposal to be applied in a Ketch system.
private PersonIdent author;
private String message;
private PushCertificate pushCert;
+
+ private List<ProposedTimestamp> timestamps;
private final List<Runnable> listeners = new CopyOnWriteArrayList<>();
private final AtomicReference<State> state = new AtomicReference<>(NEW);
return this;
}
+ /**
+ * @return timestamps that Ketch must block for. These may have been used as
+ * commit times inside the objects involved in the proposal.
+ */
+ public List<ProposedTimestamp> getProposedTimestamps() {
+ if (timestamps != null) {
+ return timestamps;
+ }
+ return Collections.emptyList();
+ }
+
+ /**
+ * Request the proposal to wait for the affected timestamps to resolve.
+ *
+ * @param ts
+ * @return {@code this}.
+ */
+ public Proposal addProposedTimestamp(ProposedTimestamp ts) {
+ if (timestamps == null) {
+ timestamps = new ArrayList<>(4);
+ }
+ timestamps.add(ts);
+ return this;
+ }
+
/**
* Add a callback to be invoked when the proposal is done.
* <p>
import static org.eclipse.jgit.internal.ketch.Proposal.State.RUNNING;
import java.io.IOException;
+import java.time.Duration;
+import java.util.ArrayList;
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 java.util.concurrent.TimeoutException;
+import java.util.stream.Collectors;
import org.eclipse.jgit.annotations.Nullable;
import org.eclipse.jgit.internal.storage.reftree.Command;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.ReceiveCommand;
+import org.eclipse.jgit.util.time.ProposedTimestamp;
/** A {@link Round} that aggregates and sends user {@link Proposal}s. */
class ProposalRound extends Round {
}
try {
ObjectId id;
- try (Repository git = leader.openRepository()) {
- id = insertProposals(git);
+ try (Repository git = leader.openRepository();
+ ProposedTimestamp ts = getSystem().getClock().propose()) {
+ id = insertProposals(git, ts);
+ blockUntil(ts);
}
runAsync(id);
} catch (NoOp e) {
}
}
- private ObjectId insertProposals(Repository git)
+ private ObjectId insertProposals(Repository git, ProposedTimestamp ts)
throws IOException, NoOp {
ObjectId id;
try (ObjectInserter inserter = git.newObjectInserter()) {
// TODO(sop) Process signed push certificates.
if (queuedTree != null) {
- id = insertSingleProposal(git, inserter);
+ id = insertSingleProposal(git, ts, inserter);
} else {
- id = insertMultiProposal(git, inserter);
+ id = insertMultiProposal(git, ts, inserter);
}
stageCommands = makeStageList(git, inserter);
return id;
}
- private ObjectId insertSingleProposal(Repository git,
+ private ObjectId insertSingleProposal(Repository git, ProposedTimestamp ts,
ObjectInserter inserter) throws IOException, NoOp {
// Fast path: tree is passed in with all proposals applied.
ObjectId treeId = queuedTree.writeTree(inserter);
if (!ObjectId.zeroId().equals(acceptedOldIndex)) {
b.setParentId(acceptedOldIndex);
}
- b.setCommitter(leader.getSystem().newCommitter());
+ b.setCommitter(leader.getSystem().newCommitter(ts));
b.setAuthor(p.getAuthor() != null ? p.getAuthor() : b.getCommitter());
b.setMessage(message(p));
return inserter.insert(b);
}
- private ObjectId insertMultiProposal(Repository git,
+ private ObjectId insertMultiProposal(Repository git, ProposedTimestamp ts,
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
}
}
- PersonIdent committer = leader.getSystem().newCommitter();
+ PersonIdent committer = leader.getSystem().newCommitter(ts);
for (Proposal p : todo) {
if (!tree.apply(p.getCommands())) {
// This should not occur, previously during queuing the
return b.makeStageList(newObjs, git, inserter);
}
+ private void blockUntil(ProposedTimestamp ts)
+ throws TimeIsUncertainException {
+ List<ProposedTimestamp> times = todo.stream()
+ .flatMap(p -> p.getProposedTimestamps().stream())
+ .collect(Collectors.toCollection(ArrayList::new));
+ times.add(ts);
+
+ try {
+ Duration maxWait = getSystem().getMaxWaitForMonotonicClock();
+ ProposedTimestamp.blockUntil(times, maxWait);
+ } catch (InterruptedException | TimeoutException e) {
+ throw new TimeIsUncertainException(e);
+ }
+ }
private static class NoOp extends Exception {
private static final long serialVersionUID = 1L;
this.acceptedOldIndex = head;
}
+ KetchSystem getSystem() {
+ return leader.getSystem();
+ }
+
/**
* Creates a commit for {@code refs/txn/accepted} and calls
* {@link #runAsync(AnyObjectId)} to begin execution of the round across
--- /dev/null
+/*
+ * 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 org.eclipse.jgit.internal.JGitText;
+
+class TimeIsUncertainException extends IOException {
+ private static final long serialVersionUID = 1L;
+
+ TimeIsUncertainException() {
+ super(JGitText.get().timeIsUncertain);
+ }
+
+ TimeIsUncertainException(Exception e) {
+ super(JGitText.get().timeIsUncertain, e);
+ }
+}
import java.io.IOException;
import java.text.MessageFormat;
+import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
+import java.util.concurrent.TimeoutException;
import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.lib.RefUpdate.Result;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.PushCertificate;
import org.eclipse.jgit.transport.ReceiveCommand;
+import org.eclipse.jgit.util.time.ProposedTimestamp;
/**
* Batch of reference updates to be applied to a repository.
* server is making changes to more than one reference at a time.
*/
public class BatchRefUpdate {
+ /**
+ * Maximum delay the calling thread will tolerate while waiting for a
+ * {@code MonotonicClock} to resolve associated {@link ProposedTimestamp}s.
+ * <p>
+ * A default of 5 seconds was chosen by guessing. A common assumption is
+ * clock skew between machines on the same LAN using an NTP server also on
+ * the same LAN should be under 5 seconds. 5 seconds is also not that long
+ * for a large `git push` operation to complete.
+ */
+ private static final Duration MAX_WAIT = Duration.ofSeconds(5);
+
private final RefDatabase refdb;
/** Commands to apply during this batch. */
/** Push options associated with this update. */
private List<String> pushOptions;
+ /** Associated timestamps that should be blocked on before update. */
+ private List<ProposedTimestamp> timestamps;
+
/**
* Initialize a new batch update.
*
return pushOptions;
}
+ /**
+ * @return list of timestamps the batch must wait for.
+ * @since 4.6
+ */
+ public List<ProposedTimestamp> getProposedTimestamps() {
+ if (timestamps != null) {
+ return Collections.unmodifiableList(timestamps);
+ }
+ return Collections.emptyList();
+ }
+
+ /**
+ * Request the batch to wait for the affected timestamps to resolve.
+ *
+ * @param ts
+ * @return {@code this}.
+ * @since 4.6
+ */
+ public BatchRefUpdate addProposedTimestamp(ProposedTimestamp ts) {
+ if (timestamps == null) {
+ timestamps = new ArrayList<>(4);
+ }
+ timestamps.add(ts);
+ return this;
+ }
+
/**
* Execute this batch update.
* <p>
}
return;
}
+ if (!blockUntilTimestamps(MAX_WAIT)) {
+ return;
+ }
if (options != null) {
pushOptions = options;
monitor.endTask();
}
+ /**
+ * Wait for timestamps to be in the past, aborting commands on timeout.
+ *
+ * @param maxWait
+ * maximum amount of time to wait for timestamps to resolve.
+ * @return true if timestamps were successfully waited for; false if
+ * commands were aborted.
+ * @since 4.6
+ */
+ protected boolean blockUntilTimestamps(Duration maxWait) {
+ if (timestamps == null) {
+ return true;
+ }
+ try {
+ ProposedTimestamp.blockUntil(timestamps, maxWait);
+ return true;
+ } catch (TimeoutException | InterruptedException e) {
+ String msg = JGitText.get().timeIsUncertain;
+ for (ReceiveCommand c : commands) {
+ if (c.getResult() == NOT_ATTEMPTED) {
+ c.setResult(REJECTED_OTHER_REASON, msg);
+ }
+ }
+ return false;
+ }
+ }
+
/**
* Execute this batch update without option strings.
*
import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.util.SystemReader;
+import org.eclipse.jgit.util.time.ProposedTimestamp;
/**
* A combination of a person identity and time in Git.
this(aName, aEmailAddress, SystemReader.getInstance().getCurrentTime());
}
+ /**
+ * Construct a new {@link PersonIdent} with current time.
+ *
+ * @param aName
+ * @param aEmailAddress
+ * @param when
+ */
+ public PersonIdent(String aName, String aEmailAddress,
+ ProposedTimestamp when) {
+ this(aName, aEmailAddress, when.millis());
+ }
+
/**
* Copy a PersonIdent, but alter the clone's time stamp
*
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectChecker;
import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.time.MonotonicClock;
+import org.eclipse.jgit.util.time.MonotonicSystemClock;
/**
* Interface to read values from the system.
*/
public abstract long getCurrentTime();
+ /**
+ * @return clock instance preferred by this system.
+ * @since 4.6
+ */
+ public MonotonicClock getClock() {
+ return new MonotonicSystemClock();
+ }
+
/**
* @param when TODO
* @return the local time zone
--- /dev/null
+/*
+ * 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.util.time;
+
+import java.time.Duration;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A provider of time.
+ * <p>
+ * Clocks should provide wall clock time, obtained from a reasonable clock
+ * source, such as the local system clock.
+ * <p>
+ * MonotonicClocks provide the following behavior, with the assertion always
+ * being true if {@link ProposedTimestamp#blockUntil(Duration)} is used:
+ *
+ * <pre>
+ * MonotonicClock clk = ...;
+ * long r1;
+ * try (ProposedTimestamp t1 = clk.propose()) {
+ * r1 = t1.millis();
+ * t1.blockUntil(...);
+ * }
+ *
+ * try (ProposedTimestamp t2 = clk.propose()) {
+ * assert t2.millis() > r1;
+ * }
+ * </pre>
+ *
+ * @since 4.6
+ */
+public interface MonotonicClock {
+ /**
+ * Obtain a timestamp close to "now".
+ * <p>
+ * Proposed times are close to "now", but may not yet be certainly in the
+ * past. This allows the calling thread to interleave other useful work
+ * while waiting for the clock instance to create an assurance it will never
+ * in the future propose a time earlier than the returned time.
+ * <p>
+ * A hypothetical implementation could read the local system clock (managed
+ * by NTP) and return that proposal, concurrently sending network messages
+ * to closely collaborating peers in the same cluster to also ensure their
+ * system clocks are ahead of this time. In such an implementation the
+ * {@link ProposedTimestamp#blockUntil(Duration)} method would wait for
+ * replies from the peers indicating their own system clocks have moved past
+ * the proposed time.
+ *
+ * @return "now". The value can be immediately accessed by
+ * {@link ProposedTimestamp#read(TimeUnit)} and friends, but the
+ * caller must use {@link ProposedTimestamp#blockUntil(Duration)} to
+ * ensure ordering holds.
+ */
+ ProposedTimestamp propose();
+}
--- /dev/null
+/*
+ * 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.util.time;
+
+import static java.util.concurrent.TimeUnit.MICROSECONDS;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+
+import java.time.Duration;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * A {@link MonotonicClock} based on {@code System.currentTimeMillis}.
+ *
+ * @since 4.6
+ */
+public class MonotonicSystemClock implements MonotonicClock {
+ private static final AtomicLong before = new AtomicLong();
+
+ private static long nowMicros() {
+ long now = MILLISECONDS.toMicros(System.currentTimeMillis());
+ for (;;) {
+ long o = before.get();
+ long n = Math.max(o + 1, now);
+ if (before.compareAndSet(o, n)) {
+ return n;
+ }
+ }
+ }
+
+ @Override
+ public ProposedTimestamp propose() {
+ final long u = nowMicros();
+ return new ProposedTimestamp() {
+ @Override
+ public long read(TimeUnit unit) {
+ return unit.convert(u, MICROSECONDS);
+ }
+
+ @Override
+ public void blockUntil(Duration maxWait) {
+ // Assume system clock never goes backwards.
+ }
+ };
+ }
+}
--- /dev/null
+/*
+ * 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.util.time;
+
+import static java.util.concurrent.TimeUnit.MICROSECONDS;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+
+import java.sql.Timestamp;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Date;
+import java.util.Iterator;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * A timestamp generated by {@link MonotonicClock#propose()}.
+ * <p>
+ * ProposedTimestamp implements AutoCloseable so that implementations can
+ * release resources associated with obtaining certainty about time elapsing.
+ * For example the constructing MonotonicClock may start network IO with peers
+ * when creating the ProposedTimestamp, and {@link #close()} can ensure those
+ * network resources are released in a timely fashion.
+ *
+ * @since 4.6
+ */
+public abstract class ProposedTimestamp implements AutoCloseable {
+ /**
+ * Wait for several timestamps.
+ *
+ * @param times
+ * timestamps to wait on.
+ * @param maxWait
+ * how long to wait for the timestamps.
+ * @throws InterruptedException
+ * current thread was interrupted before the waiting process
+ * completed normally.
+ * @throws TimeoutException
+ * the timeout was reached without the proposed timestamp become
+ * certainly in the past.
+ */
+ public static void blockUntil(Iterable<ProposedTimestamp> times,
+ Duration maxWait) throws TimeoutException, InterruptedException {
+ Iterator<ProposedTimestamp> itr = times.iterator();
+ if (!itr.hasNext()) {
+ return;
+ }
+
+ long now = System.currentTimeMillis();
+ long deadline = now + maxWait.toMillis();
+ for (;;) {
+ long w = deadline - now;
+ if (w < 0) {
+ throw new TimeoutException();
+ }
+ itr.next().blockUntil(Duration.ofMillis(w));
+ if (itr.hasNext()) {
+ now = System.currentTimeMillis();
+ } else {
+ break;
+ }
+ }
+ }
+
+ /**
+ * Read the timestamp as {@code unit} since the epoch.
+ * <p>
+ * The timestamp value for a specific {@code ProposedTimestamp} object never
+ * changes, and can be read before {@link #blockUntil(Duration)}.
+ *
+ * @param unit
+ * what unit to return the timestamp in. The timestamp will be
+ * rounded if the unit is bigger than the clock's granularity.
+ * @return {@code unit} since the epoch.
+ */
+ public abstract long read(TimeUnit unit);
+
+ /**
+ * Wait for this proposed timestamp to be certainly in the recent past.
+ * <p>
+ * This method forces the caller to wait up to {@code timeout} for
+ * {@code this} to pass sufficiently into the past such that the creating
+ * {@link MonotonicClock} instance will not create an earlier timestamp.
+ *
+ * @param maxWait
+ * how long the implementation may block the caller.
+ * @throws InterruptedException
+ * current thread was interrupted before the waiting process
+ * completed normally.
+ * @throws TimeoutException
+ * the timeout was reached without the proposed timestamp
+ * becoming certainly in the past.
+ */
+ public abstract void blockUntil(Duration maxWait)
+ throws InterruptedException, TimeoutException;
+
+ /** @return milliseconds since epoch; {@code read(MILLISECONDS}). */
+ public long millis() {
+ return read(MILLISECONDS);
+ }
+
+ /** @return microseconds since epoch; {@code read(MICROSECONDS}). */
+ public long micros() {
+ return read(MICROSECONDS);
+ }
+
+ /** @return time since epoch, with up to microsecond resolution. */
+ public Instant instant() {
+ long usec = micros();
+ long secs = usec / 1000000L;
+ long nanos = (usec % 1000000L) * 1000L;
+ return Instant.ofEpochSecond(secs, nanos);
+ }
+
+ /** @return time since epoch, with up to microsecond resolution. */
+ public Timestamp timestamp() {
+ return Timestamp.from(instant());
+ }
+
+ /** @return time since epoch, with up to millisecond resolution. */
+ public Date date() {
+ return new Date(millis());
+ }
+
+ /** Release resources allocated by this timestamp. */
+ @Override
+ public void close() {
+ // Do nothing by default.
+ }
+
+ @Override
+ public String toString() {
+ return instant().toString();
+ }
+}