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;
                 }
+              }
             }
-        }
-    };
-
+          };
 }