Use ticker in Deadline testing to remove flakes
diff --git a/core/src/main/java/io/grpc/Deadline.java b/core/src/main/java/io/grpc/Deadline.java
index 8ef421f..1c30aa7 100644
--- a/core/src/main/java/io/grpc/Deadline.java
+++ b/core/src/main/java/io/grpc/Deadline.java
@@ -31,6 +31,7 @@
package io.grpc;
+import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import java.util.concurrent.ScheduledExecutorService;
@@ -42,6 +43,7 @@
*/
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/262")
public final class Deadline implements Comparable<Deadline> {
+ private static final SystemTicker SYSTEM_TICKER = new SystemTicker();
/**
* Create a deadline that will expire at the specified offset from the current system clock.
@@ -50,14 +52,26 @@
* @return A new deadline.
*/
public static Deadline after(long duration, TimeUnit units) {
- Preconditions.checkNotNull(units);
- return new Deadline(System.nanoTime(), units.toNanos(duration), true);
+ return after(duration, units, SYSTEM_TICKER);
}
+ @VisibleForTesting
+ static Deadline after(long duration, TimeUnit units, Ticker ticker) {
+ Preconditions.checkNotNull(units);
+ return new Deadline(ticker, units.toNanos(duration), true);
+ }
+
+ private final Ticker ticker;
private final long deadlineNanos;
private volatile boolean expired;
- private Deadline(long baseInstant, long offset, boolean baseInstantAlreadyExpired) {
+ private Deadline(Ticker ticker, long offset, boolean baseInstantAlreadyExpired) {
+ this(ticker, ticker.read(), offset, baseInstantAlreadyExpired);
+ }
+
+ private Deadline(Ticker ticker, long baseInstant, long offset,
+ boolean baseInstantAlreadyExpired) {
+ this.ticker = ticker;
if (offset > 0 && Long.MAX_VALUE - offset < baseInstant) {
deadlineNanos = Long.MAX_VALUE;
expired = false;
@@ -76,7 +90,7 @@
*/
public boolean isExpired() {
if (!expired) {
- if (deadlineNanos <= System.nanoTime()) {
+ if (deadlineNanos <= ticker.read()) {
expired = true;
} else {
return false;
@@ -108,7 +122,7 @@
if (offset == 0) {
return this;
}
- return new Deadline(deadlineNanos, units.toNanos(offset), isExpired());
+ return new Deadline(ticker, deadlineNanos, units.toNanos(offset), isExpired());
}
/**
@@ -118,7 +132,7 @@
* long ago the deadline expired.
*/
public long timeRemaining(TimeUnit unit) {
- final long nowNanos = System.nanoTime();
+ final long nowNanos = ticker.read();
if (!expired && deadlineNanos <= nowNanos) {
expired = true;
}
@@ -134,7 +148,7 @@
public ScheduledFuture<?> runOnExpiration(Runnable task, ScheduledExecutorService scheduler) {
Preconditions.checkNotNull(task, "task");
Preconditions.checkNotNull(scheduler, "scheduler");
- return scheduler.schedule(task, deadlineNanos - System.nanoTime(), TimeUnit.NANOSECONDS);
+ return scheduler.schedule(task, deadlineNanos - ticker.read(), TimeUnit.NANOSECONDS);
}
@Override
@@ -152,4 +166,17 @@
}
return 0;
}
+
+ /** Time source representing nanoseconds since fixed but arbitrary point in time. */
+ abstract static class Ticker {
+ /** Returns the number of nanoseconds since this source's epoch. */
+ public abstract long read();
+ }
+
+ private static class SystemTicker extends Ticker {
+ @Override
+ public long read() {
+ return System.nanoTime();
+ }
+ }
}
diff --git a/core/src/test/java/io/grpc/CallOptionsTest.java b/core/src/test/java/io/grpc/CallOptionsTest.java
index b3eec9c..3a85db0 100644
--- a/core/src/test/java/io/grpc/CallOptionsTest.java
+++ b/core/src/test/java/io/grpc/CallOptionsTest.java
@@ -34,7 +34,6 @@
import static com.google.common.truth.Truth.assertAbout;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
-import static io.grpc.DeadlineTest.extractRemainingTime;
import static io.grpc.testing.DeadlineSubject.deadline;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.MINUTES;
@@ -56,7 +55,8 @@
@RunWith(JUnit4.class)
public class CallOptionsTest {
private String sampleAuthority = "authority";
- private Deadline sampleDeadline = Deadline.after(1, NANOSECONDS);
+ private Deadline.Ticker ticker = new DeadlineTest.FakeTicker();
+ private Deadline sampleDeadline = Deadline.after(1, NANOSECONDS, ticker);
private Key<String> sampleKey = Attributes.Key.of("sample");
private Attributes sampleAffinity = Attributes.newBuilder().set(sampleKey, "blah").build();
private CallOptions allSet = CallOptions.DEFAULT
@@ -146,8 +146,7 @@
@Test
public void toStringMatches_withDeadline() {
- assertAbout(deadline()).that(extractRemainingTime(allSet.toString()))
- .isWithin(20, MILLISECONDS).of(allSet.getDeadline());
+ allSet.toString().contains("1 ns from now");
}
@Test
diff --git a/core/src/test/java/io/grpc/DeadlineTest.java b/core/src/test/java/io/grpc/DeadlineTest.java
index 5581c63..be4f0bb 100644
--- a/core/src/test/java/io/grpc/DeadlineTest.java
+++ b/core/src/test/java/io/grpc/DeadlineTest.java
@@ -33,74 +33,77 @@
import static com.google.common.truth.Truth.assertAbout;
import static io.grpc.testing.DeadlineSubject.deadline;
-import static java.util.concurrent.TimeUnit.NANOSECONDS;
-import static java.util.concurrent.TimeUnit.SECONDS;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
import com.google.common.truth.Truth;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
+import org.mockito.ArgumentCaptor;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.Executors;
+import java.util.Random;
+import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
+import java.util.concurrent.atomic.AtomicBoolean;
/**
* Tests for {@link Context}.
*/
@RunWith(JUnit4.class)
public class DeadlineTest {
+ private FakeTicker ticker = new FakeTicker();
- // Allowed inaccuracy when comparing the remaining time of a deadline.
- private final long maxDelta = TimeUnit.MILLISECONDS.toNanos(20);
+ @Test
+ public void defaultTickerIsSystemTicker() {
+ Deadline d = Deadline.after(0, TimeUnit.SECONDS);
+ ticker.reset(System.nanoTime());
+ Deadline reference = Deadline.after(0, TimeUnit.SECONDS, ticker);
+ // Allow inaccuracy to account for system time advancing during test.
+ assertAbout(deadline()).that(d).isWithin(20, TimeUnit.MILLISECONDS).of(reference);
+ }
@Test
public void immediateDeadlineIsExpired() {
- Deadline deadline = Deadline.after(0, TimeUnit.SECONDS);
+ Deadline deadline = Deadline.after(0, TimeUnit.SECONDS, ticker);
assertTrue(deadline.isExpired());
}
@Test
public void shortDeadlineEventuallyExpires() throws Exception {
- Deadline d = Deadline.after(100, TimeUnit.MILLISECONDS);
+ Deadline d = Deadline.after(100, TimeUnit.MILLISECONDS, ticker);
assertTrue(d.timeRemaining(TimeUnit.NANOSECONDS) > 0);
assertFalse(d.isExpired());
- Thread.sleep(101);
+ ticker.increment(101, TimeUnit.MILLISECONDS);
assertTrue(d.isExpired());
- assertFalse(d.timeRemaining(TimeUnit.NANOSECONDS) > 0);
- assertAbout(deadline()).that(d).isWithin(maxDelta, NANOSECONDS).of(Deadline.after(0, SECONDS));
+ assertEquals(-1, d.timeRemaining(TimeUnit.MILLISECONDS));
}
@Test
public void deadlineMatchesLongValue() {
- long minutes = Deadline.after(10, TimeUnit.MINUTES).timeRemaining(TimeUnit.MINUTES);
-
- assertTrue(minutes + " != " + 10, Math.abs(minutes - 10) <= 1);
+ assertEquals(10, Deadline.after(10, TimeUnit.MINUTES, ticker).timeRemaining(TimeUnit.MINUTES));
}
@Test
public void pastDeadlineIsExpired() {
- Deadline d = Deadline.after(-1, TimeUnit.SECONDS);
+ Deadline d = Deadline.after(-1, TimeUnit.SECONDS, ticker);
assertTrue(d.isExpired());
-
- assertAbout(deadline()).that(d).isWithin(maxDelta, NANOSECONDS).of(Deadline.after(-1, SECONDS));
+ assertEquals(-1000, d.timeRemaining(TimeUnit.MILLISECONDS));
}
@Test
public void deadlineDoesNotOverflowOrUnderflow() {
- Deadline after = Deadline.after(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
+ Deadline after = Deadline.after(Long.MAX_VALUE, TimeUnit.NANOSECONDS, ticker);
assertFalse(after.isExpired());
- Deadline before = Deadline.after(-Long.MAX_VALUE, TimeUnit.NANOSECONDS);
+ Deadline before = Deadline.after(-Long.MAX_VALUE, TimeUnit.NANOSECONDS, ticker);
assertTrue(before.isExpired());
assertTrue(before.isBefore(after));
@@ -108,108 +111,122 @@
@Test
public void beforeExpiredDeadlineIsExpired() {
- Deadline base = Deadline.after(0, TimeUnit.SECONDS);
+ Deadline base = Deadline.after(0, TimeUnit.SECONDS, ticker);
assertTrue(base.isExpired());
assertTrue(base.offset(-1, TimeUnit.SECONDS).isExpired());
}
@Test
public void afterExpiredDeadlineIsNotExpired() {
- Deadline base = Deadline.after(0, TimeUnit.SECONDS);
+ Deadline base = Deadline.after(0, TimeUnit.SECONDS, ticker);
assertTrue(base.isExpired());
assertFalse(base.offset(100, TimeUnit.SECONDS).isExpired());
}
@Test
public void zeroOffsetIsSameDeadline() {
- Deadline base = Deadline.after(0, TimeUnit.SECONDS);
+ Deadline base = Deadline.after(0, TimeUnit.SECONDS, ticker);
assertSame(base, base.offset(0, TimeUnit.SECONDS));
}
@Test
public void runOnEventualExpirationIsExecuted() throws Exception {
- Deadline base = Deadline.after(50, TimeUnit.MILLISECONDS);
- final CountDownLatch latch = new CountDownLatch(1);
+ Deadline base = Deadline.after(50, TimeUnit.MICROSECONDS, ticker);
+ ScheduledExecutorService mockScheduler = mock(ScheduledExecutorService.class);
+ final AtomicBoolean executed = new AtomicBoolean();
base.runOnExpiration(
new Runnable() {
@Override
public void run() {
- latch.countDown();
+ executed.set(true);
}
- }, Executors.newSingleThreadScheduledExecutor());
- if (!latch.await(70, TimeUnit.MILLISECONDS)) {
- fail("Deadline listener did not execute in time");
- }
+ }, mockScheduler);
+ assertFalse(executed.get());
+ ArgumentCaptor<Runnable> runnableCaptor = ArgumentCaptor.forClass(Runnable.class);
+ verify(mockScheduler).schedule(runnableCaptor.capture(), eq(50000L), eq(TimeUnit.NANOSECONDS));
+ runnableCaptor.getValue().run();
+ assertTrue(executed.get());
}
@Test
- public void runOnAlreadyExpiredIsExecuted() throws Exception {
- Deadline base = Deadline.after(0, TimeUnit.MILLISECONDS);
- final CountDownLatch latch = new CountDownLatch(1);
+ public void runOnAlreadyExpiredIsExecutedOnExecutor() throws Exception {
+ Deadline base = Deadline.after(0, TimeUnit.MICROSECONDS, ticker);
+ ScheduledExecutorService mockScheduler = mock(ScheduledExecutorService.class);
+ final AtomicBoolean executed = new AtomicBoolean();
base.runOnExpiration(
new Runnable() {
@Override
public void run() {
- latch.countDown();
+ executed.set(true);
}
- }, Executors.newSingleThreadScheduledExecutor());
- if (!latch.await(10, TimeUnit.MILLISECONDS)) {
- fail("Deadline listener did not execute in time");
- }
+ }, mockScheduler);
+ assertFalse(executed.get());
+ ArgumentCaptor<Runnable> runnableCaptor = ArgumentCaptor.forClass(Runnable.class);
+ verify(mockScheduler).schedule(runnableCaptor.capture(), eq(0L), eq(TimeUnit.NANOSECONDS));
+ runnableCaptor.getValue().run();
+ assertTrue(executed.get());
}
@Test
public void toString_exact() {
- Deadline d = Deadline.after(0, TimeUnit.MILLISECONDS);
-
- assertAbout(deadline()).that(extractRemainingTime(d.toString()))
- .isWithin(maxDelta, NANOSECONDS).of(d);
+ Deadline d = Deadline.after(0, TimeUnit.MILLISECONDS, ticker);
+ assertEquals("0 ns from now", d.toString());
}
@Test
public void toString_after() {
- Deadline d = Deadline.after(-1, TimeUnit.HOURS);
-
- assertAbout(deadline()).that(extractRemainingTime(d.toString()))
- .isWithin(maxDelta, NANOSECONDS).of(d);
+ Deadline d = Deadline.after(-1, TimeUnit.MINUTES, ticker);
+ assertEquals("-60000000000 ns from now", d.toString());
}
@Test
public void compareTo_greater() {
- Deadline d1 = Deadline.after(10, TimeUnit.SECONDS);
- Deadline d2 = Deadline.after(10, TimeUnit.SECONDS);
- // Assume that two calls take more than 1 ns.
+ Deadline d1 = Deadline.after(10, TimeUnit.SECONDS, ticker);
+ ticker.increment(1, TimeUnit.NANOSECONDS);
+ Deadline d2 = Deadline.after(10, TimeUnit.SECONDS, ticker);
Truth.assertThat(d2).isGreaterThan(d1);
}
@Test
public void compareTo_less() {
- Deadline d1 = Deadline.after(10, TimeUnit.SECONDS);
- Deadline d2 = Deadline.after(10, TimeUnit.SECONDS);
- // Assume that two calls take more than 1 ns.
+ Deadline d1 = Deadline.after(10, TimeUnit.SECONDS, ticker);
+ ticker.increment(1, TimeUnit.NANOSECONDS);
+ Deadline d2 = Deadline.after(10, TimeUnit.SECONDS, ticker);
Truth.assertThat(d1).isLessThan(d2);
}
@Test
public void compareTo_same() {
- Deadline d1 = Deadline.after(10, TimeUnit.SECONDS);
- Deadline d2 = d1.offset(0, TimeUnit.SECONDS);
+ Deadline d1 = Deadline.after(10, TimeUnit.SECONDS, ticker);
+ Deadline d2 = Deadline.after(10, TimeUnit.SECONDS, ticker);
Truth.assertThat(d1).isEquivalentAccordingToCompareTo(d2);
}
@Test
public void toString_before() {
- Deadline d = Deadline.after(10, TimeUnit.SECONDS);
-
- assertAbout(deadline()).that(extractRemainingTime(d.toString()))
- .isWithin(maxDelta, NANOSECONDS).of(d);
+ Deadline d = Deadline.after(12, TimeUnit.MICROSECONDS, ticker);
+ assertEquals("12000 ns from now", d.toString());
}
- static Deadline extractRemainingTime(String deadlineStr) {
- final Pattern p = Pattern.compile(".*?(-?\\d+) ns from now.*");
- Matcher m = p.matcher(deadlineStr);
- assertTrue(deadlineStr, m.matches());
- assertEquals(deadlineStr, 1, m.groupCount());
- return Deadline.after(Long.valueOf(m.group(1)), NANOSECONDS);
+ static class FakeTicker extends Deadline.Ticker {
+ private static final Random random = new Random();
+
+ private long time = random.nextLong();
+
+ @Override
+ public long read() {
+ return time;
+ }
+
+ public void reset(long time) {
+ this.time = time;
+ }
+
+ public void increment(long period, TimeUnit unit) {
+ if (period < 0) {
+ throw new IllegalArgumentException();
+ }
+ this.time += unit.toNanos(period);
+ }
}
}