From 9796060a4975b4881c8a14c1a99bb4ca19bdb4a6 Mon Sep 17 00:00:00 2001 From: Guus Bloemsma <sheepdreamofandroids@users.noreply.github.com> Date: Tue, 2 Nov 2021 07:54:04 +0100 Subject: [PATCH] fixes for https://github.com/ben-manes/caffeine/issues/7#issuecomment-956281417 (#621) made more fields final applied google formatter gave the tests more slack to also succeed consistently on Windows 10 --- .../bulkloader/CoalescingBulkloader.java | 308 ++++++++++-------- .../bulkloader/CoalescingBulkloaderTest.java | 270 ++++++++------- 2 files changed, 316 insertions(+), 262 deletions(-) diff --git a/examples/coalescing-bulkloader/src/main/java/com/github/benmanes/caffeine/examples/coalescing/bulkloader/CoalescingBulkloader.java b/examples/coalescing-bulkloader/src/main/java/com/github/benmanes/caffeine/examples/coalescing/bulkloader/CoalescingBulkloader.java index 5c3b1a8dbd..cf1f25e438 100644 --- a/examples/coalescing-bulkloader/src/main/java/com/github/benmanes/caffeine/examples/coalescing/bulkloader/CoalescingBulkloader.java +++ b/examples/coalescing-bulkloader/src/main/java/com/github/benmanes/caffeine/examples/coalescing/bulkloader/CoalescingBulkloader.java @@ -15,10 +15,8 @@ */ package com.github.benmanes.caffeine.examples.coalescing.bulkloader; -import static java.util.concurrent.TimeUnit.MILLISECONDS; -import static java.util.stream.Collectors.toMap; - import com.github.benmanes.caffeine.cache.AsyncCacheLoader; + import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; @@ -37,155 +35,185 @@ import java.util.function.Function; import java.util.stream.Stream; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static java.util.stream.Collectors.toMap; + /** - * An implementation of {@link AsyncCacheLoader} that delays fetching a bit until "enough" keys are collected - * to do a bulk call. The assumption is that doing a bulk call is so much more efficient that it is worth - * the wait. + * An implementation of {@link AsyncCacheLoader} that delays fetching a bit until "enough" keys are + * collected to do a bulk call. The assumption is that doing a bulk call is so much more efficient + * that it is worth the wait. * - * @param <Key> the type of the key in the cache + * @param <Key> the type of the key in the cache * @param <Value> the type of the value in the cache * @author complain to: guus@bloemsma.net */ public class CoalescingBulkloader<Key, Value> implements AsyncCacheLoader<Key, Value> { - private final Consumer<Collection<WaitingKey>> bulkLoader; - private int maxLoadSize; // maximum number of keys to load in one call - private long maxDelay; // maximum time between request of a value and loading it - private volatile Queue<WaitingKey> waitingKeys = new ConcurrentLinkedQueue<>(); - private ScheduledExecutorService timer = Executors.newSingleThreadScheduledExecutor(); - private ScheduledFuture<?> schedule; - // Queue.size() is expensive, so here we keep track of the queue size separately - private AtomicInteger size = new AtomicInteger(0); - - private final class WaitingKey { - Key key; - CompletableFuture<Value> future; - long waitingSince; + private final Consumer<Collection<WaitingKey<Key, Value>>> bulkLoader; + private final int maxLoadSize; // maximum number of keys to load in one call + private final long maxDelay; // maximum time between request of a value and loading it + private final Queue<WaitingKey<Key, Value>> waitingKeys = new ConcurrentLinkedQueue<>(); + private final ScheduledExecutorService timer = Executors.newSingleThreadScheduledExecutor(); + private ScheduledFuture<?> schedule; + // Queue.size() is expensive, so here we keep track of the queue size separately + private final AtomicInteger size = new AtomicInteger(0); + + private static class WaitingKey<Key, Value> { + private final Key key; + private final CompletableFuture<Value> future = new CompletableFuture<>(); + private final long waitingSince = System.currentTimeMillis(); + + private WaitingKey(Key key) { + this.key = key; } - - /** - * Wraps a bulk loader that returns values in the same order as the keys. - * - * @param maxLoadSize Maximum number of keys per bulk load - * @param maxDelay Maximum time to wait before bulk load is executed - * @param load Loader that takes keys and returns a future with the values in the same order as the keys. - * Extra values are ignored. Missing values lead to a {@link java.util.NoSuchElementException} - * for the corresponding future. - */ - public static <Key, Value> CoalescingBulkloader<Key, Value> byOrder(int maxLoadSize, long maxDelay, - final Function<Stream<Key>, CompletableFuture<Stream<Value>>> load) { - return new CoalescingBulkloader<>(maxLoadSize, maxDelay, toLoad -> { - final Stream<Key> keys = toLoad.stream().map(wk -> wk.key); - load.apply(keys).thenAccept(values -> { - final Iterator<Value> iv = values.iterator(); - for (CoalescingBulkloader<Key, Value>.WaitingKey waitingKey : toLoad) { - if (iv.hasNext()) - waitingKey.future.complete(iv.next()); - else - waitingKey.future.completeExceptionally(new NoSuchElementException("No value for key " + waitingKey.key)); - } - }); + } + + /** + * Wraps a bulk loader that returns values in the same order as the keys. + * + * @param maxLoadSize Maximum number of keys per bulk load + * @param maxDelay Maximum time to wait before bulk load is executed + * @param load Loader that takes keys and returns a future with the values in the same order as + * the keys. Extra values are ignored. Missing values lead to a {@link + * java.util.NoSuchElementException} for the corresponding future. + */ + public static <Key, Value> CoalescingBulkloader<Key, Value> byOrder( + int maxLoadSize, + long maxDelay, + final Function<Stream<Key>, CompletableFuture<Stream<Value>>> load) { + return new CoalescingBulkloader<>( + maxLoadSize, + maxDelay, + toLoad -> { + final Stream<Key> keys = toLoad.stream().map(wk -> wk.key); + load.apply(keys) + .thenAccept( + values -> { + final Iterator<Value> iv = values.iterator(); + for (CoalescingBulkloader.WaitingKey<Key, Value> waitingKey : toLoad) { + if (iv.hasNext()) waitingKey.future.complete(iv.next()); + else + waitingKey.future.completeExceptionally( + new NoSuchElementException("No value for key " + waitingKey.key)); + } + }); }); - } - - /** - * Wraps a bulk loader that returns values in a map accessed by key. - * - * @param maxLoadSize Maximum number of keys per bulk load - * @param maxDelay Maximum time to wait before bulk load is executed - * @param load Loader that takes keys and returns a future with a map with keys and values. - * Extra values are ignored. Missing values lead to a {@link java.util.NoSuchElementException} - * for the corresponding future. - */ - public static <Key, Value> CoalescingBulkloader<Key, Value> byMap(int maxLoadSize, long maxDelay, - final Function<Stream<Key>, CompletableFuture<Map<Key, Value>>> load) { - return new CoalescingBulkloader<>(maxLoadSize, maxDelay, toLoad -> { - final Stream<Key> keys = toLoad.stream().map(wk -> wk.key); - load.apply(keys).thenAccept(values -> { - for (CoalescingBulkloader<Key, Value>.WaitingKey waitingKey : toLoad) { - if (values.containsKey(waitingKey.key)) + } + + /** + * Wraps a bulk loader that returns values in a map accessed by key. + * + * @param maxLoadSize Maximum number of keys per bulk load + * @param maxDelay Maximum time to wait before bulk load is executed + * @param load Loader that takes keys and returns a future with a map with keys and values. Extra + * values are ignored. Missing values lead to a {@link java.util.NoSuchElementException} for + * the corresponding future. + */ + public static <Key, Value> CoalescingBulkloader<Key, Value> byMap( + int maxLoadSize, + long maxDelay, + final Function<Stream<Key>, CompletableFuture<Map<Key, Value>>> load) { + return new CoalescingBulkloader<>( + maxLoadSize, + maxDelay, + toLoad -> { + final Stream<Key> keys = toLoad.stream().map(wk -> wk.key); + load.apply(keys) + .thenAccept( + values -> { + for (CoalescingBulkloader.WaitingKey<Key, Value> waitingKey : toLoad) { + if (values.containsKey(waitingKey.key)) waitingKey.future.complete(values.get(waitingKey.key)); - else - waitingKey.future.completeExceptionally(new NoSuchElementException("No value for key " + waitingKey.key)); - } - }); + else + waitingKey.future.completeExceptionally( + new NoSuchElementException("No value for key " + waitingKey.key)); + } + }); }); + } + + /** + * Wraps a bulk loader that returns intermediate values from which keys and values can be + * extracted. + * + * @param <Intermediate> Some internal type from which keys and values can be extracted. + * @param maxLoadSize Maximum number of keys per bulk load + * @param maxDelay Maximum time to wait before bulk load is executed + * @param keyExtractor How to extract key from intermediate value + * @param valueExtractor How to extract value from intermediate value + * @param load Loader that takes keys and returns a future with a map with keys and values. Extra + * values are ignored. Missing values lead to a {@link java.util.NoSuchElementException} for + * the corresponding future. + */ + public static <Key, Value, Intermediate> CoalescingBulkloader<Key, Value> byExtraction( + int maxLoadSize, + long maxDelay, + final Function<Intermediate, Key> keyExtractor, + final Function<Intermediate, Value> valueExtractor, + final Function<Stream<Key>, CompletableFuture<Stream<Intermediate>>> load) { + return byMap( + maxLoadSize, + maxDelay, + keys -> + load.apply(keys) + .thenApply( + intermediates -> intermediates.collect(toMap(keyExtractor, valueExtractor)))); + } + + private CoalescingBulkloader( + int maxLoadSize, long maxDelay, Consumer<Collection<WaitingKey<Key, Value>>> bulkLoader) { + this.bulkLoader = bulkLoader; + assert maxLoadSize > 0; + assert maxDelay > 0; + this.maxLoadSize = maxLoadSize; + this.maxDelay = maxDelay; + } + + @Override + public CompletableFuture<Value> asyncLoad(Key key, Executor executor) { + final WaitingKey<Key, Value> waitingKey = new WaitingKey<>(key); + waitingKeys.add(waitingKey); + + if (size.incrementAndGet() >= maxLoadSize) { + doLoad(); + } else if (schedule == null || schedule.isDone()) { + startWaiting(); } - /** - * Wraps a bulk loader that returns intermediate values from which keys and values can be extracted. - * - * @param <Intermediate> Some internal type from which keys and values can be extracted. - * @param maxLoadSize Maximum number of keys per bulk load - * @param maxDelay Maximum time to wait before bulk load is executed - * @param keyExtractor How to extract key from intermediate value - * @param valueExtractor How to extract value from intermediate value - * @param load Loader that takes keys and returns a future with a map with keys and values. - * Extra values are ignored. Missing values lead to a {@link java.util.NoSuchElementException} - * for the corresponding future. - */ - public static <Key, Value, Intermediate> CoalescingBulkloader<Key, Value> byExtraction(int maxLoadSize, long maxDelay, - final Function<Intermediate, Key> keyExtractor, - final Function<Intermediate, Value> valueExtractor, - final Function<Stream<Key>, CompletableFuture<Stream<Intermediate>>> load) { - return byMap(maxLoadSize, maxDelay, - keys -> load.apply(keys).thenApply( - intermediates -> intermediates.collect(toMap(keyExtractor, valueExtractor)))); - } - - private CoalescingBulkloader(int maxLoadSize, long maxDelay, Consumer<Collection<WaitingKey>> bulkLoader) { - this.bulkLoader = bulkLoader; - assert maxLoadSize > 0; - assert maxDelay > 0; - this.maxLoadSize = maxLoadSize; - this.maxDelay = maxDelay; - } - - @Override public CompletableFuture<Value> asyncLoad(Key key, Executor executor) { - final WaitingKey waitingKey = new WaitingKey(); - waitingKey.key = key; - waitingKey.future = new CompletableFuture<>(); - waitingKey.waitingSince = System.currentTimeMillis(); - waitingKeys.add(waitingKey); - - if (size.incrementAndGet() >= maxLoadSize) { - doLoad(); - } else if (schedule == null || schedule.isDone()) { - startWaiting(); - } - - return waitingKey.future; - } - - synchronized private void startWaiting() { - schedule = timer.schedule(this::doLoad, maxDelay, MILLISECONDS); - } - - synchronized private void doLoad() { - schedule.cancel(false); - do { - List<WaitingKey> toLoad = new ArrayList<>(Math.min(size.get(), maxLoadSize)); - int counter = maxLoadSize; - while (counter > 0) { - final WaitingKey waitingKey = waitingKeys.poll(); - if (waitingKey == null) - break; - else { - toLoad.add(waitingKey); - counter--; - } - } - - final int taken = maxLoadSize - counter; - if (taken > 0) { - bulkLoader.accept(toLoad); - size.updateAndGet(oldSize -> oldSize - taken); - } - - } while (size.get() >= maxLoadSize); - final WaitingKey nextWaitingKey = waitingKeys.peek(); - if (nextWaitingKey != null) { - schedule = timer.schedule(this::doLoad, nextWaitingKey.waitingSince + maxDelay - System.currentTimeMillis(), MILLISECONDS); + return waitingKey.future; + } + + private synchronized void startWaiting() { + if (schedule != null) schedule.cancel(false); + schedule = timer.schedule(this::doLoad, maxDelay, MILLISECONDS); + } + + private synchronized void doLoad() { + do { + List<WaitingKey<Key, Value>> toLoad = new ArrayList<>(Math.min(size.get(), maxLoadSize)); + int counter = maxLoadSize; + while (counter > 0) { + final WaitingKey<Key, Value> waitingKey = waitingKeys.poll(); + if (waitingKey == null) break; + else { + toLoad.add(waitingKey); + counter--; } + } + + final int taken = maxLoadSize - counter; + if (taken > 0) { + bulkLoader.accept(toLoad); + size.updateAndGet(oldSize -> oldSize - taken); + } + + } while (size.get() >= maxLoadSize); + final WaitingKey<Key, Value> nextWaitingKey = waitingKeys.peek(); + if (nextWaitingKey != null) { + schedule = + timer.schedule( + this::doLoad, + nextWaitingKey.waitingSince + maxDelay - System.currentTimeMillis(), + MILLISECONDS); } - + } } diff --git a/examples/coalescing-bulkloader/src/test/java/com/github/benmanes/caffeine/examples/coalescing/bulkloader/CoalescingBulkloaderTest.java b/examples/coalescing-bulkloader/src/test/java/com/github/benmanes/caffeine/examples/coalescing/bulkloader/CoalescingBulkloaderTest.java index 9653c30023..a7dcf2e8dd 100644 --- a/examples/coalescing-bulkloader/src/test/java/com/github/benmanes/caffeine/examples/coalescing/bulkloader/CoalescingBulkloaderTest.java +++ b/examples/coalescing-bulkloader/src/test/java/com/github/benmanes/caffeine/examples/coalescing/bulkloader/CoalescingBulkloaderTest.java @@ -15,25 +15,8 @@ */ package com.github.benmanes.caffeine.examples.coalescing.bulkloader; -import static com.github.benmanes.caffeine.examples.coalescing.bulkloader.CoalescingBulkloader.byExtraction; -import static com.github.benmanes.caffeine.examples.coalescing.bulkloader.CoalescingBulkloader.byMap; -import static com.github.benmanes.caffeine.examples.coalescing.bulkloader.CoalescingBulkloader.byOrder; -import static java.util.concurrent.TimeUnit.MILLISECONDS; -import static java.util.function.Function.identity; -import static java.util.stream.Collectors.toMap; -import static org.hamcrest.Matchers.sameInstance; -import static org.hamcrest.core.Is.is; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertThat; - import com.github.benmanes.caffeine.cache.AsyncLoadingCache; import com.github.benmanes.caffeine.cache.Caffeine; -import java.util.Arrays; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Function; -import java.util.stream.Stream; import org.awaitility.Awaitility; import org.junit.Rule; import org.junit.Test; @@ -44,120 +27,163 @@ import org.junit.runners.Parameterized.Parameters; import org.junit.runners.model.Statement; -@RunWith(Parameterized.class) -public final class CoalescingBulkloaderTest { - - private final Function<Function<Stream<Integer>, Stream<Integer>>, CoalescingBulkloader<Integer, Integer>> cbl; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; +import java.util.stream.Stream; - public CoalescingBulkloaderTest(Function<Function<Stream<Integer>, Stream<Integer>>, CoalescingBulkloader<Integer, Integer>> cbl) { - this.cbl = cbl; - } +import static com.github.benmanes.caffeine.examples.coalescing.bulkloader.CoalescingBulkloader.byExtraction; +import static com.github.benmanes.caffeine.examples.coalescing.bulkloader.CoalescingBulkloader.byMap; +import static com.github.benmanes.caffeine.examples.coalescing.bulkloader.CoalescingBulkloader.byOrder; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static java.util.function.Function.identity; +import static java.util.stream.Collectors.toMap; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.sameInstance; +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertFalse; - private final static int maxLoadSize = 10; - private final static int maxDelay = 50; - private final static int delta = 5; - private final static int actualLoadTime = 20; - - @Parameters - public static List<Function<Function<Stream<Integer>, Stream<Integer>>, CoalescingBulkloader<Integer, Integer>>> loaderTypes() { - return Arrays.asList( - fun -> byOrder(maxLoadSize, maxDelay, - ints -> CompletableFuture.supplyAsync(() -> fun.apply(ints))), - fun -> byMap(maxLoadSize, maxDelay, - ints -> CompletableFuture.supplyAsync(() -> fun.apply(ints).collect(toMap(identity(), identity())))), - fun -> byExtraction(maxLoadSize, maxDelay, identity(), identity(), - ints -> CompletableFuture.supplyAsync(() -> fun.apply(ints))) - ); - } +@RunWith(Parameterized.class) +public final class CoalescingBulkloaderTest { - private AsyncLoadingCache<Integer, Integer> createCache(AtomicInteger loaderCalled) { - return Caffeine.newBuilder().buildAsync(cbl.apply(ints -> { - loaderCalled.incrementAndGet(); - try { - Thread.sleep(actualLoadTime); - } catch (InterruptedException e) { - e.printStackTrace(); - } - return ints; - })); + private final Function< + Function<Stream<Integer>, Stream<Integer>>, CoalescingBulkloader<Integer, Integer>> + cbl; + + public CoalescingBulkloaderTest( + Function<Function<Stream<Integer>, Stream<Integer>>, CoalescingBulkloader<Integer, Integer>> + cbl) { + this.cbl = cbl; + } + + private static final int maxLoadSize = 10; + private static final int maxDelay = 100; + private static final int delta = 20; + private static final int actualLoadTime = 50; + + @Parameters + public static List< + Function< + Function<Stream<Integer>, Stream<Integer>>, CoalescingBulkloader<Integer, Integer>>> + loaderTypes() { + return Arrays.asList( + fun -> + byOrder( + maxLoadSize, + maxDelay, + ints -> CompletableFuture.supplyAsync(() -> fun.apply(ints))), + fun -> + byMap( + maxLoadSize, + maxDelay, + ints -> + CompletableFuture.supplyAsync( + () -> fun.apply(ints).collect(toMap(identity(), identity())))), + fun -> + byExtraction( + maxLoadSize, + maxDelay, + identity(), + identity(), + ints -> CompletableFuture.supplyAsync(() -> fun.apply(ints)))); + } + + private AsyncLoadingCache<Integer, Integer> createCache(AtomicInteger loaderCalled) { + return Caffeine.newBuilder() + .buildAsync( + cbl.apply( + ints -> { + loaderCalled.incrementAndGet(); + try { + Thread.sleep(actualLoadTime); + } catch (InterruptedException e) { + e.printStackTrace(); + } + return ints; + })); + } + + @Test + public void maxDelayIsNotMissedTooMuch() throws InterruptedException { + AtomicInteger loaderCalled = new AtomicInteger(0); + final AsyncLoadingCache<Integer, Integer> cache = createCache(loaderCalled); + + // a cache get won't take too long + final CompletableFuture<Integer> result = cache.get(1); + Awaitility.await() + .pollThread(Thread::new) + .pollInterval(1, MILLISECONDS) + .between(maxDelay - delta, MILLISECONDS, maxDelay + delta, MILLISECONDS) + .untilAtomic(loaderCalled, is(1)); + assertFalse("delay in load", result.isDone()); + Thread.sleep(actualLoadTime); + assertThat(result.getNow(0), is(1)); + } + + @Test + public void whenEnoughKeysAreRequestedTheLoadWillHappenImmediately() throws InterruptedException { + AtomicInteger loaderCalled = new AtomicInteger(0); + final AsyncLoadingCache<Integer, Integer> cache = createCache(loaderCalled); + + CompletableFuture<Integer>[] results = new CompletableFuture[maxLoadSize]; + for (int i = 0; i < maxLoadSize - 1; i++) results[i] = cache.get(i); + Thread.sleep(delta); + // requesting 9 keys does not trigger a load + assertThat(loaderCalled.get(), is(0)); + + for (int i = 0; i < maxLoadSize - 1; i++) { + final CompletableFuture<Integer> result = cache.get(i); + assertThat(result, sameInstance(results[i])); + assertFalse("no load therefore unknown result", result.isDone()); } - - @Test - public void maxDelayIsNotMissedTooMuch() throws InterruptedException { - AtomicInteger loaderCalled = new AtomicInteger(0); - final AsyncLoadingCache<Integer, Integer> cache = createCache(loaderCalled); - - // a cache get won't take too long - final CompletableFuture<Integer> result = cache.get(1); - Awaitility.await().pollThread(Thread::new).pollInterval(1, MILLISECONDS) - .between(maxDelay - delta, MILLISECONDS, maxDelay + delta, MILLISECONDS) - .untilAtomic(loaderCalled, is(1)); - assertFalse("delay in load", result.isDone()); - Thread.sleep(actualLoadTime); - assertThat(result.getNow(0), is(1)); + Thread.sleep(delta); + // requesting the same 9 keys still doesn't trigger a load + assertThat(loaderCalled.get(), is(0)); + + // requesting one more key will trigger immediately + results[maxLoadSize - 1] = cache.get(maxLoadSize - 1); + Awaitility.await() + .pollInterval(1, MILLISECONDS) + .atMost(delta, MILLISECONDS) + .untilAtomic(loaderCalled, is(1)); + + // values are not immediately available because of the sleep in the loader + for (int i = 0; i < maxLoadSize; i++) { + assertThat(results[i].getNow(-1), is(-1)); } - - @Test - public void whenEnoughKeysAreRequestedTheLoadWillHappenImmediately() throws InterruptedException { - AtomicInteger loaderCalled = new AtomicInteger(0); - final AsyncLoadingCache<Integer, Integer> cache = createCache(loaderCalled); - - CompletableFuture<Integer>[] results = new CompletableFuture[maxLoadSize]; - for (int i = 0; i < maxLoadSize - 1; i++) - results[i] = cache.get(i); - Thread.sleep(delta); - // requesting 9 keys does not trigger a load - assertThat(loaderCalled.get(), is(0)); - - for (int i = 0; i < maxLoadSize - 1; i++) { - final CompletableFuture<Integer> result = cache.get(i); - assertThat(result, sameInstance(results[i])); - assertFalse("no load therefore unknown result", result.isDone()); - } - Thread.sleep(delta); - // requesting the same 9 keys still doesn't trigger a load - assertThat(loaderCalled.get(), is(0)); - - // requesting one more key will trigger immediately - results[maxLoadSize - 1] = cache.get(maxLoadSize - 1); - Awaitility.await().pollInterval(1, MILLISECONDS) - .atMost(delta, MILLISECONDS) - .untilAtomic(loaderCalled, is(Integer.valueOf(1))); - - // values are not immediately available because of the sleep in the loader - for (int i = 0; i < maxLoadSize; i++) { - assertThat(results[i].getNow(-1), is(-1)); - } - Thread.sleep(actualLoadTime + delta); - // slept enough - for (int i = 0; i < maxLoadSize; i++) { - assertThat(results[i].getNow(-1), is(i)); - } - + Thread.sleep(actualLoadTime + delta); + // slept enough + for (int i = 0; i < maxLoadSize; i++) { + assertThat(results[i].getNow(-1), is(i)); } + } + + @Rule + // Because the jvm may have to warm up or whatever other influences, timing may be off, causing + // these tests to fail. + // So retry a couple of times. + public TestRule retry = + (final Statement base, final Description description) -> + new Statement() { + + @Override + public void evaluate() throws Throwable { + try_(3); + } - @Rule - // Because the jvm may have to warm up or whatever other influences, timing may be off, causing these tests to fail. - // So retry a couple of times. - public TestRule retry = (final Statement base, final Description description) -> new Statement() { - - @Override - public void evaluate() throws Throwable { - try_(3); - } - - void try_(int tries) throws Throwable { - try { + void try_(int tries) throws Throwable { + try { base.evaluate(); - } catch (Throwable throwable) { - System.err.println(description.getDisplayName() + " failed, " + (tries - 1) + " attempts left."); - if (tries > 1) - try_(tries - 1); + } catch (Throwable throwable) { + System.err.println( + description.getDisplayName() + " failed, " + (tries - 1) + " attempts left."); + if (tries > 1) try_(tries - 1); else { - throw throwable; + throw throwable; } + } } - } - }; - + }; }