diff --git a/org.eclipse.jgit.junit/META-INF/MANIFEST.MF b/org.eclipse.jgit.junit/META-INF/MANIFEST.MF index d7ce17542..3597a508b 100644 --- a/org.eclipse.jgit.junit/META-INF/MANIFEST.MF +++ b/org.eclipse.jgit.junit/META-INF/MANIFEST.MF @@ -21,6 +21,7 @@ Import-Package: org.eclipse.jgit.api;version="[4.6.0,4.7.0)", 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)", @@ -33,4 +34,5 @@ Export-Package: org.eclipse.jgit.junit;version="4.6.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" diff --git a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/LocalDiskRepositoryTestCase.java b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/LocalDiskRepositoryTestCase.java index c27d03d29..dc2e8bfb7 100644 --- a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/LocalDiskRepositoryTestCase.java +++ b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/LocalDiskRepositoryTestCase.java @@ -122,13 +122,8 @@ public void setUp() throws Exception { 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); diff --git a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/MockSystemReader.java b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/MockSystemReader.java index 28a95569e..6faa2ece4 100644 --- a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/MockSystemReader.java +++ b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/MockSystemReader.java @@ -50,10 +50,12 @@ 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; @@ -61,6 +63,8 @@ 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. @@ -146,6 +150,27 @@ public long getCurrentTime() { 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. * diff --git a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/time/MonotonicFakeClock.java b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/time/MonotonicFakeClock.java new file mode 100644 index 000000000..f09d303d5 --- /dev/null +++ b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/time/MonotonicFakeClock.java @@ -0,0 +1,90 @@ +/* + * 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. + } + }; + } +} diff --git a/org.eclipse.jgit/META-INF/MANIFEST.MF b/org.eclipse.jgit/META-INF/MANIFEST.MF index 03a90f9b0..20fd73073 100644 --- a/org.eclipse.jgit/META-INF/MANIFEST.MF +++ b/org.eclipse.jgit/META-INF/MANIFEST.MF @@ -60,10 +60,7 @@ Export-Package: org.eclipse.jgit.annotations;version="4.6.0", 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, @@ -136,7 +133,8 @@ Export-Package: org.eclipse.jgit.annotations;version="4.6.0", 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)", diff --git a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties index 8a16a3b19..b9b74bfc2 100644 --- a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties +++ b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties @@ -606,6 +606,7 @@ tagAlreadyExists=tag ''{0}'' already exists 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. diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java index 3577c4b28..ada5bf711 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java @@ -666,6 +666,7 @@ public static JGitText get() { /***/ public String tagOnRepoWithoutHEADCurrentlyNotSupported; /***/ public String transactionAborted; /***/ public String theFactoryMustNotBeNull; + /***/ public String timeIsUncertain; /***/ public String timerAlreadyTerminated; /***/ public String tooManyIncludeRecursions; /***/ public String topologicalSortRequired; 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 index 014eab2b4..1221ddd8d 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ElectionRound.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ElectionRound.java @@ -43,10 +43,12 @@ 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; @@ -55,6 +57,7 @@ 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; @@ -75,9 +78,11 @@ class ElectionRound extends Round { 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); } @@ -91,12 +96,17 @@ long getTerm() { 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; @@ -116,7 +126,7 @@ private ObjectId bumpTerm(Repository git, ObjectInserter inserter) msg.append(' ').append(tag); } - b.setAuthor(leader.getSystem().newCommitter()); + b.setAuthor(leader.getSystem().newCommitter(ts)); b.setCommitter(b.getAuthor()); b.setMessage(msg.toString()); @@ -138,4 +148,12 @@ private static long parseTerm(List footer) { } 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); + } + } } 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 index 71e872e3f..33f526e52 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchSystem.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchSystem.java @@ -53,6 +53,7 @@ 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; @@ -67,6 +68,9 @@ 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; @@ -88,6 +92,7 @@ public static ScheduledExecutorService defaultExecutor() { } private final ScheduledExecutorService executor; + private final MonotonicClock clock; private final String txnNamespace; private final String txnAccepted; private final String txnCommitted; @@ -95,7 +100,7 @@ public static ScheduledExecutorService defaultExecutor() { /** 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); } /** @@ -103,13 +108,17 @@ public KetchSystem() { * * @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; @@ -121,6 +130,28 @@ public ScheduledExecutorService getExecutor() { 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. * @@ -145,27 +176,32 @@ public String getTxnStage() { 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. *

* 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. *

* 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. *

* The default implementation generates a few bytes of random data. * 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 index e297bca45..907eecbef 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/LocalReplica.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/LocalReplica.java @@ -64,6 +64,8 @@ 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 { @@ -119,9 +121,11 @@ protected void startPush(final ReplicaPushRequest req) { 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); @@ -139,8 +143,8 @@ protected void blockingFetch(Repository repo, ReplicaFetchRequest req) 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(); @@ -156,7 +160,8 @@ private void update(Repository git, ReplicaPushRequest req) } 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); 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 index 0876eb5db..12d3f4c9c 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/Proposal.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/Proposal.java @@ -67,6 +67,7 @@ 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. @@ -123,6 +124,8 @@ public boolean isDone() { private PersonIdent author; private String message; private PushCertificate pushCert; + + private List timestamps; private final List listeners = new CopyOnWriteArrayList<>(); private final AtomicReference state = new AtomicReference<>(NEW); @@ -222,6 +225,31 @@ public Proposal setPushCertificate(@Nullable PushCertificate cert) { 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 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. *

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 index d34477ab2..ddd7059fc 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ProposalRound.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ProposalRound.java @@ -46,12 +46,16 @@ 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; @@ -65,6 +69,7 @@ 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 { @@ -123,8 +128,10 @@ void start() throws IOException { } 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) { @@ -143,16 +150,16 @@ void start() throws IOException { } } - 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); @@ -161,7 +168,7 @@ private ObjectId insertProposals(Repository git) 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); @@ -183,13 +190,13 @@ private ObjectId insertSingleProposal(Repository git, 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 @@ -208,7 +215,7 @@ private ObjectId insertMultiProposal(Repository git, } } - 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 @@ -292,6 +299,20 @@ private List makeStageList(Repository git, return b.makeStageList(newObjs, git, inserter); } + private void blockUntil(ProposedTimestamp ts) + throws TimeIsUncertainException { + List 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; 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 index 1335b85cc..dd8e568c7 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/Round.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/Round.java @@ -75,6 +75,10 @@ abstract class Round { 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 diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/TimeIsUncertainException.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/TimeIsUncertainException.java new file mode 100644 index 000000000..7223f553c --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/TimeIsUncertainException.java @@ -0,0 +1,60 @@ +/* + * 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); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/BatchRefUpdate.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/BatchRefUpdate.java index 8550ec3a3..653c9f66b 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/BatchRefUpdate.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/BatchRefUpdate.java @@ -49,18 +49,21 @@ 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. @@ -69,6 +72,17 @@ * 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. + *

+ * 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. */ @@ -95,6 +109,9 @@ public class BatchRefUpdate { /** Push options associated with this update. */ private List pushOptions; + /** Associated timestamps that should be blocked on before update. */ + private List timestamps; + /** * Initialize a new batch update. * @@ -313,6 +330,32 @@ public List getPushOptions() { return pushOptions; } + /** + * @return list of timestamps the batch must wait for. + * @since 4.6 + */ + public List 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. *

@@ -348,6 +391,9 @@ public void execute(RevWalk walk, ProgressMonitor monitor, } return; } + if (!blockUntilTimestamps(MAX_WAIT)) { + return; + } if (options != null) { pushOptions = options; @@ -432,6 +478,33 @@ public void execute(RevWalk walk, ProgressMonitor monitor, 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. * diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/PersonIdent.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/PersonIdent.java index e08a98529..0deb542cf 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/PersonIdent.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/PersonIdent.java @@ -53,6 +53,7 @@ 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. @@ -188,6 +189,18 @@ public PersonIdent(final String aName, final String aEmailAddress) { 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 * diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/SystemReader.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/SystemReader.java index dc10dab17..b36fc2391 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/SystemReader.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/SystemReader.java @@ -60,6 +60,8 @@ 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. @@ -230,6 +232,14 @@ protected final void setPlatformChecker() { */ 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 diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/time/MonotonicClock.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/time/MonotonicClock.java new file mode 100644 index 000000000..794d85190 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/time/MonotonicClock.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2016, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.eclipse.jgit.util.time; + +import java.time.Duration; +import java.util.concurrent.TimeUnit; + +/** + * A provider of time. + *

+ * Clocks should provide wall clock time, obtained from a reasonable clock + * source, such as the local system clock. + *

+ * MonotonicClocks provide the following behavior, with the assertion always + * being true if {@link ProposedTimestamp#blockUntil(Duration)} is used: + * + *

+ *   MonotonicClock clk = ...;
+ *   long r1;
+ *   try (ProposedTimestamp t1 = clk.propose()) {
+ *   	r1 = t1.millis();
+ *   	t1.blockUntil(...);
+ *   }
+ *
+ *   try (ProposedTimestamp t2 = clk.propose()) {
+ *   	assert t2.millis() > r1;
+ *   }
+ * 
+ * + * @since 4.6 + */ +public interface MonotonicClock { + /** + * Obtain a timestamp close to "now". + *

+ * 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. + *

+ * 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(); +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/time/MonotonicSystemClock.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/time/MonotonicSystemClock.java new file mode 100644 index 000000000..a9f483063 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/time/MonotonicSystemClock.java @@ -0,0 +1,87 @@ +/* + * 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. + } + }; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/time/ProposedTimestamp.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/time/ProposedTimestamp.java new file mode 100644 index 000000000..c09ab32b6 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/time/ProposedTimestamp.java @@ -0,0 +1,176 @@ +/* + * 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()}. + *

+ * 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 times, + Duration maxWait) throws TimeoutException, InterruptedException { + Iterator 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. + *

+ * 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. + *

+ * 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(); + } +}